diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..7b737155f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "Javascript SDK", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-bookworm", + + "postCreateCommand": "cd /workspaces/javascript-sdk && npm install -g npm && npm install", + + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "esbenp.prettier-vscode", + "Gruntfuggly.todo-tree", + "github.vscode-github-actions", + "ms-vscode.test-adapter-converter", + "vitest.explorer" + ] + } + } +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..d5ad68d3b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +*.tests.js +*.umdtests.js +test_data.js diff --git a/.eslintrc.js b/.eslintrc.js index 9778987dc..20feb2cb4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,48 +1,35 @@ module.exports = { - "globals": { - "console": true, - "exports": true, - "mocha": true, - "it": true, - "describe": true, - "before": true, - "beforeEach": true, - "after": true, - "afterEach": true, - "sinon": true + env: { + browser: true, + commonjs: true, + node: true, }, - "rules": { - "strict": 0, - "quotes": [1, "single"], - "no-native-reassign": 0, - "camelcase": 0, - "dot-notation": 0, - "no-debugger": 1, - "comma-dangle": [0, "always-multiline"], - "no-underscore-dangle": 0, - "no-unused-vars": [1, {"vars": "all", "args": "none"}], - "no-trailing-spaces": 1, - "key-spacing": 1, - "no-unused-expressions": 1, - "no-multi-spaces": 1, - "no-use-before-define": 0, - "space-infix-ops": 1, - "no-console": 0, - "comma-spacing": 1, - "no-alert": 1, - "no-empty": 1, - "no-extra-bind": 1, - "eol-last": 1, - "eqeqeq": 1, - "semi": 1, - "no-multi-str": 0, - "no-extra-semi": 1, - "new-cap": 0, - "consistent-return": 0, - "no-extra-boolean-cast": 0, - "no-mixed-spaces-and-tabs": 1, - "no-shadow": 1, - "no-sequences": 1, - "handle-callback-err": 1 - } -} + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly', + Promise: 'readonly', + }, + parserOptions: { + // Note: The TS compiler determines what syntax is accepted. We're using TS version 4.0.3. + // This seems to correspond to "2020" for this setting. + ecmaVersion: 2020, + sourceType: 'module', + }, + overrides: [ + { + 'files': ['*.ts'], + 'rules': { + '@typescript-eslint/explicit-module-boundary-types': ['error'] + } + } + ], + rules: { + 'no-prototype-builtins': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + }, +}; diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 000000000..855cdf50d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,106 @@ +name: 🐞 Bug +description: File a bug/issue +title: "[BUG] " +labels: ["bug", "needs-triage"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: SDK Version + description: Version of the SDK in use? + validations: + required: true +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 1. With this config... + 1. Run '...' + 1. See error... + validations: + required: true +- type: dropdown + attributes: + label: SDK Type + description: Please select the type of JS SDK. + multiple: false + options: + - Browser + - Node + - React Native + - Edge/Lite + validations: + required: true +- type: textarea + attributes: + label: Node Version + description: What version of Node are you using? + validations: + required: false +- type: textarea + attributes: + label: Browsers impacted + description: What browsers are impacted? + validations: + required: false +- type: textarea + attributes: + label: Link + description: Link to code demonstrating the problem. + validations: + required: false +- type: textarea + attributes: + label: Logs + description: Logs/stack traces related to the problem (⚠️do not include sensitive information). + validations: + required: false +- type: dropdown + attributes: + label: Severity + description: What is the severity of the problem? + multiple: true + options: + - Blocking development + - Affecting users + - Minor issue + validations: + required: false +- type: textarea + attributes: + label: Workaround/Solution + description: Do you have any workaround or solution in mind for the problem? + validations: + required: false +- type: textarea + attributes: + label: "Recent Change" + description: Has this issue started happening after an update or experiment change? + validations: + required: false +- type: textarea + attributes: + label: Conflicts + description: Are there other libraries/dependencies potentially in conflict? + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml new file mode 100644 index 000000000..79c53247b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml @@ -0,0 +1,45 @@ +name: ✨Enhancement +description: Create a new ticket for a Enhancement/Tech-initiative for the benefit of the SDK which would be considered for a minor version update. +title: "[ENHANCEMENT] <title>" +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: "Description" + description: Briefly describe the enhancement in a few sentences. + placeholder: Short description... + validations: + required: true + - type: textarea + id: benefits + attributes: + label: "Benefits" + description: How would the enhancement benefit to your product or usage? + placeholder: Benefits... + validations: + required: true + - type: textarea + id: detail + attributes: + label: "Detail" + description: How would you like the enhancement to work? Please provide as much detail as possible + placeholder: Detailed description... + validations: + required: false + - type: textarea + id: examples + attributes: + label: "Examples" + description: Are there any examples of this enhancement in other products/services? If so, please provide links or references. + placeholder: Links/References... + validations: + required: false + - type: textarea + id: risks + attributes: + label: "Risks/Downsides" + description: Do you think this enhancement could have any potential downsides or risks? + placeholder: Risks/Downsides... + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md new file mode 100644 index 000000000..a061f3356 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md @@ -0,0 +1,4 @@ +<!-- + Thanks for filing in issue! Are you requesting a new feature? If so, please share your feedback with us on the following link. +--> +## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..d28ef3dd4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💡Feature Requests + url: https://feedback.optimizely.com/ + about: Feedback requesting a new feature can be shared here. \ No newline at end of file diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 01f3e9b9e..000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,39 +0,0 @@ -<!-- - Thanks for filing in issue! Are you proposing an enhancement or reporting a bug? - - If proposing an enhancement, please describe your use case in as much detail as you think is needed to convey the value of the enhancement. ---> -## How would the enhancement work? - -## When would the enhancement be useful? - -<!-- - If reporting a bug, please include the following info: ---> - -## What I wanted to do - -## What I expected to happen - -## What actually happened - -## Steps to reproduce -Link to repository that can reproduce the issue: <link> - -<!-- - OR provide the following. - If possible, whittle down your issue into a [short, self-contained, correct example](http://sscce.org/). ---> - -**`@optimizely/optimizely-sdk` version:** - -<!-- ...and whichever of the following are applicable: --> - -**Browser and version:** - -**`node` version:** - -**`npm` version:** - -Versions of any other relevant tools (like module bundlers, transpilers, etc.): - diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 000000000..70b391e18 --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,60 @@ +name: Reusable action of running integration of production suite + +on: + workflow_call: + inputs: + FULLSTACK_TEST_REPO: + required: false + type: string + secrets: + CI_USER_TOKEN: + required: true + TRAVIS_COM_TOKEN: + required: true +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + # You should create a personal access token and store it in your repository + token: ${{ secrets.CI_USER_TOKEN }} + repository: 'optimizely/travisci-tools' + path: 'home/runner/travisci-tools' + ref: 'master' + - name: set SDK Branch if PR + env: + HEAD_REF: ${{ github.head_ref }} + if: ${{ github.event_name == 'pull_request' }} + run: | + echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV + echo "TRAVIS_BRANCH=$HEAD_REF" >> $GITHUB_ENV + - name: set SDK Branch if not pull request + env: + REF_NAME: ${{ github.ref_name }} + if: ${{ github.event_name != 'pull_request' }} + run: | + echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV + echo "TRAVIS_BRANCH=$REF_NAME" >> $GITHUB_ENV + - name: Trigger build + env: + SDK: javascript + FULLSTACK_TEST_REPO: ${{ inputs.FULLSTACK_TEST_REPO }} + BUILD_NUMBER: ${{ github.run_id }} + TESTAPP_BRANCH: master + GITHUB_TOKEN: ${{ secrets.CI_USER_TOKEN }} + EVENT_TYPE: ${{ github.event_name }} + GITHUB_CONTEXT: ${{ toJson(github) }} + #REPO_SLUG: ${{ github.repository }} + PULL_REQUEST_SLUG: ${{ github.repository }} + UPSTREAM_REPO: ${{ github.repository }} + PULL_REQUEST_SHA: ${{ github.event.pull_request.head.sha }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + UPSTREAM_SHA: ${{ github.sha }} + TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} + EVENT_MESSAGE: ${{ github.event.message }} + HOME: 'home/runner' + run: | + CLIENT=node home/runner/travisci-tools/trigger-script-with-status-update.sh + # Run only browser builds when it's FSC not for FPS. + [ "$FULLSTACK_TEST_REPO" == "ProdTesting" ] || CLIENT=browser home/runner/travisci-tools/trigger-script-with-status-update.sh \ No newline at end of file diff --git a/.github/workflows/javascript.yml b/.github/workflows/javascript.yml new file mode 100644 index 000000000..c097ff585 --- /dev/null +++ b/.github/workflows/javascript.yml @@ -0,0 +1,104 @@ +name: Javascript + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + lint_markdown_files: + uses: optimizely/javascript-sdk/.github/workflows/lint_markdown.yml@master + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 16 + cache-dependency-path: ./package-lock.json + cache: 'npm' + - name: Run linting + working-directory: . + run: | + npm install + npm run lint + + integration_tests: + uses: optimizely/javascript-sdk/.github/workflows/integration_test.yml@master + secrets: + CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} + TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} + + fullstack_production_suite: + uses: optimizely/javascript-sdk/.github/workflows/integration_test.yml@master + with: + FULLSTACK_TEST_REPO: ProdTesting + secrets: + CI_USER_TOKEN: ${{ secrets.CI_USER_TOKEN }} + TRAVIS_COM_TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} + + # crossbrowser_and_umd_unit_tests: + # runs-on: ubuntu-latest + # env: + # BROWSER_STACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + # BROWSER_STACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + # steps: + # - uses: actions/checkout@v3 + # - name: Set up Node + # uses: actions/setup-node@v3 + # with: + # node-version: 16 + # cache: 'npm' + # cache-dependency-path: ./package-lock.json + # - name: Cross-browser and umd unit tests + # working-directory: . + # run: | + # npm install + # npm run test-ci + + unit_tests: + runs-on: ubuntu-latest + strategy: + matrix: + node: ['18', '20', '22', '24'] + steps: + - uses: actions/checkout@v3 + - name: Set up Node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + cache-dependency-path: ./package-lock.json + - name: Unit tests + working-directory: . + run: | + npm install + npm run coveralls + - name: Coveralls Parallel + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./coverage/lcov.info + flag-name: run-${{ matrix.node }} + # This is a parallel build so need this + parallel: true + base-path: . + + # As testing against multiple versions need this to + # finish the parallel build + finish: + name: Coveralls coverage + needs: unit_tests + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + path-to-lcov: ./coverage/lcov.info + parallel-finished: true + base-path: . + \ No newline at end of file diff --git a/.github/workflows/lint_markdown.yml b/.github/workflows/lint_markdown.yml new file mode 100644 index 000000000..af23e15a4 --- /dev/null +++ b/.github/workflows/lint_markdown.yml @@ -0,0 +1,19 @@ +name: Reusable action of linting markdown files + +on: [workflow_call] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.6' + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Install gem + run: | + gem install awesome_bot + - name: Run tests + run: find . -type f -name '*.md' -exec awesome_bot {} \; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..f3e710a44 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +name: Publish SDK to NPM + +on: + release: + types: [published, edited] + workflow_dispatch: {} + +jobs: + publish: + name: Publish to NPM + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.draft }} + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org/" + always-auth: "true" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + + - name: Install dependencies + run: npm install + + - id: latest-release + name: Export latest release git tag + run: | + echo "latest-release-tag=$(curl -qsSL \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/latest" \ + | jq -r .tag_name)" >> $GITHUB_OUTPUT + + - id: npm-tag + name: Determine NPM tag + env: + GITHUB_RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + VERSION=$(jq -r '.version' package.json) + LATEST_RELEASE_TAG="${{ steps.latest-release.outputs['latest-release-tag']}}" + + if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then + RELEASE_TAG=${GITHUB_REF#refs/tags/} + else + RELEASE_TAG=$GITHUB_RELEASE_TAG + fi + + if [[ $RELEASE_TAG == $LATEST_RELEASE_TAG ]]; then + echo "npm-tag=latest" >> "$GITHUB_OUTPUT" + elif [[ "$VERSION" == *"-beta"* ]]; then + echo "npm-tag=beta" >> "$GITHUB_OUTPUT" + elif [[ "$VERSION" == *"-alpha"* ]]; then + echo "npm-tag=alpha" >> "$GITHUB_OUTPUT" + elif [[ "$VERSION" == *"-rc"* ]]; then + echo "npm-tag=rc" >> "$GITHUB_OUTPUT" + else + echo "npm-tag=v$(echo $VERSION | awk -F. '{print $1}')-latest" >> "$GITHUB_OUTPUT" + fi + + - id: release + name: Test, build and publish to npm + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + run: | + if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then + DRY_RUN="--dry-run" + fi + npm publish --tag=${{ steps.npm-tag.outputs['npm-tag'] }} $DRY_RUN + + # - name: Report results to Jellyfish + # uses: optimizely/jellyfish-deployment-reporter-action@main + # if: ${{ always() && github.event_name == 'release' && (steps.release.outcome == 'success' || steps.release.outcome == 'failure') }} + # with: + # jellyfish_api_token: ${{ secrets.JELLYFISH_API_TOKEN }} + # is_successful: ${{ steps.release.outcome == 'success' }} diff --git a/.github/workflows/source_clear_crone.yml b/.github/workflows/source_clear_crone.yml new file mode 100644 index 000000000..328feb6ab --- /dev/null +++ b/.github/workflows/source_clear_crone.yml @@ -0,0 +1,18 @@ +name: Source clear + +on: + push: + branches: [ master ] + schedule: + # Runs "weekly" + - cron: '0 0 * * 0' + +jobs: + source_clear: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Source clear scan + env: + SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }} + run: curl -sSL https://download.sourceclear.com/ci.sh | bash -s - scan \ No newline at end of file diff --git a/.github/workflows/ticket_reference_check.yml b/.github/workflows/ticket_reference_check.yml new file mode 100644 index 000000000..b7d52780f --- /dev/null +++ b/.github/workflows/ticket_reference_check.yml @@ -0,0 +1,16 @@ +name: Jira ticket reference check + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +jobs: + + jira_ticket_reference_check: + runs-on: ubuntu-latest + + steps: + - name: Check for Jira ticket reference + uses: optimizely/github-action-ticket-reference-checker-public@master + with: + bodyRegex: 'FSSDK-(?<ticketNumber>\d+)' diff --git a/.gitignore b/.gitignore index 52945c239..4ab687ed5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,17 @@ -.DS_STORE +node_modules/ +optimizely-*.tgz + npm-debug.log -dist -node_modules lerna-debug.log coverage/ +dist/ +# user-specific ignores ought to be defined in user's `core.excludesfile` .idea/* +.DS_STORE + +browserstack.err +local.log + +**/*.gen.ts diff --git a/.prettierrc b/.prettierrc index 95d49510c..62301e2e3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,10 +1,10 @@ { - "printWidth": 89, + "printWidth": 120, "tabWidth": 2, "useTabs": false, - "semi": false, + "semi": true, "singleQuote": true, - "trailingComma": "all", + "trailingComma": "es5", "bracketSpacing": true, "jsxBracketSameLine": false } diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 76aa52e41..000000000 --- a/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ -language: node_js -node_js: -- '6' -- '8' -- '9' -- '10' -branches: - only: - - master - - /^\d+\.\d+\.(\d|[x]) -env: - global: - # BROWSER_STACK_USERNAME - - secure: WnyM4gMsi2n69O/YZUD/pYwHJXdKDcBv3Hwft2cCw52yYc+z75uuRgdaLKs4BPisckBtnR17dH7hKlPX3HWwjCoqQm1q5qNpbJrArWaEcbotWGF2YFy21ZZ4rKNQmJqdgRj6XFZhLHbncA8v2gQPK7F6GUJ0vsJF/kiTfxAUjefR23oorcKSQrh9BfOxNAYu2Ma92qlaaMmHYbBdlNDM45/EQE+LnPCfboCiJD/5zTYq4Q+XhKLPV01vUDU60pj9ckDNXyLj9X2BwMbAzGAPGE4qTAB/IrMndVsUXblsahtwKQ6yrsUrsdTASz8/3oNkImtY7fqU874jSeG3d7PNBfZs47zkXEVy73ZWNBgM9rzVS5cPaIU3wqpuBoXFntDJcdHQhNTWEYdxmtcTUmxKt5TdUzDhrrkcti2WVLabU3N52aOBeOM0XBpfLkbV+HT6oWi3bNUb+EDMHvCxOxsP4IoEDfFs9HMzNIO3mmC3+2DFbI7s2Mb2oacAut38MbJDYSDTOLL4smG8scA3E0RQO4r8+TNk4aRIMQc7vCKqz7PpbO7Aj9dXSpeHrDmIszSmEoQqmaaGsRBwbXRom2P8fB9FcTbd/wbsfgoFNEPz5DlbtCtCmt0pQMa+3myWveKH52WC5KlFijBSDjYOMUnXbLnj5fK5eKaWp+z6/qcNwU8= - # BROWSER_STACK_ACCESS_KEY - - secure: U0GGZw46rJowBtH9gVluIrerB40u2b3uZpH0HsOdLlsXCCaTVk4JXX/JPVPashWAFLC7Enk3UOE4ofeEpVd0wbG6CxtG9/gklc2U2tvkqsdPpFZKaRrXoUzCyyPOmHEC2mXDXctbrncmttM4APaceRfbdTBEZIIfyLJadomjWylA61szFE9IZjvJpiwJO2xa5HI9GVRu3yXJci+riJux+JsDmfJ1hNwv3waMeeg/scddUH0hfgq69ftGs8cpMlYiO20eh32S7uPF7/IJTH1fDJjVKYQZwpypkF6AeI+od5CFTY1ajb25eaBNXThLS0Bo9ZJE/8Sogvon21dEJkt/ClY6R341InbAFXZvz7jyQAisvh0I4zxcu0VUCfh7bEUl6GXMO8VJnyxHEfqB+AIT2RoMXckkhulwiNUsJYH1yJ8mjnLvZq85mWBCp4n4jg0K6Wf46lHpjnHOVpLyLyoFGfiPf90AQVL02AJ3/ia8RkMuj0Ax+AGtiTC/+wy7dsDQOif/VpBNJcx/RciQ24mYOGzAMh4GsUWnXaZ9vXSxliogVNrmIefK5invJ0omv9pIx8NZHTHYGaulh4w6JsliiEq2kH78SlyvSrcsFGTwCY97LLaxiLm/75/Zf+F7LajKC23Fbtnj/LQizitFZqGMJ09DnR52krBAeultqRq8QLM= -before_install: cd packages/optimizely-sdk -install: npm install -addons: - srcclr: true -script: npm test -after_success: npm run coveralls - -# Integration tests need to run first to reset the PR build status to pending -stages: - - 'Integration tests' - - 'Cross-browser and umd unit tests' - - 'Test' - -jobs: - include: - - stage: 'Integration tests' - merge_mode: replace - env: SDK=javascript - cache: false - language: python - before_install: skip - install: - - "pip install awscli" - before_script: - - "aws s3 cp s3://optimizely-travisci-artifacts/ci/trigger_fullstack-sdk-compat.sh ci/ && chmod u+x ci/trigger_fullstack-sdk-compat.sh" - script: - - "ci/trigger_fullstack-sdk-compat.sh" - after_success: travis_terminate 0 - - stage: Cross-browser and umd unit tests - node_js: '8' - script: npm run test-ci diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ce072c82c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 2 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ee82dd595 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1307 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [6.1.0] - September 8, 2025 + +### New Features + +- Added multi-region support for logx events ([#1072](https://github.com/optimizely/javascript-sdk/pull/1072)) + +### Performance Improvements + +- Improved performance of variations parsing from datafile ([#1080](https://github.com/optimizely/javascript-sdk/pull/1080)) +- General cleanups and improvements in event processing ([#1073](https://github.com/optimizely/javascript-sdk/pull/1073)) + +## [6.0.0] - May 29, 2025 + +### Breaking Changes + +- Modularized SDK architecture: The monolithic `createInstance` call has been split into multiple factory functions for greater flexibility and control. +- Core functionalities (project configuration, event processing, ODP, VUID, logging, and error handling) are now configured through dedicated components created via factory functions, giving you greater flexibility and control in enabling/disabling certain components and allowing optimizing the bundle size for frontend projects. +- `onReady` Promise behavior changed: It now resolves only when the SDK is ready and rejects on initialization errors. +- event processing is disabled by default and must be explicitly enabled by passing a `eventProcessor` to the client. +- Event dispatcher interface updated to use Promises instead of callbacks. +- Logging is disabled by default and must be explicitly enabled using a logger created via a factory function. +- VUID tracking is disabled by default and must be explicitly enabled by passing a `vuidManager` to the client instance. +- ODP functionality is no longer enabled by default. You must explicitly pass an `odpManager` to enable it. +- Dropped support for older browser versions and Node.js versions earlier than 18.0.0. + +### New Features +- Added support for async user profile service and async decide methods (see dcoumentation for [User Profile Service](https://docs.developers.optimizely.com/feature-experimentation/docs/implement-a-user-profile-service-for-the-javascript-sdk) and [Decide methods](https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-for-the-javascript-sdk)) + +### Migration Guide + +For detailed migration instructions, refer to the [Migration Guide](MIGRATION.md). + +### Documentation + +For more details, see the official documentation: [JavaScript SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk). + +## [5.3.5] - Jan 29, 2025 + +### Bug Fixes + +- Rollout experiment key exclusion from activate method([#949](https://github.com/optimizely/javascript-sdk/pull/949)) +- Using optimizely.readyPromise instead of optimizely.onReady to avoid setTimeout call in edge environments. ([#995](https://github.com/optimizely/javascript-sdk/pull/995)) + +## [4.10.1] - November 18, 2024 + +### Changed +- update uuid module improt and usage ([#961](https://github.com/optimizely/javascript-sdk/pull/961)) + + +## [5.3.4] - Jun 28, 2024 + +### Changed + +- crypto and text encoder polyfill addition for React native ([#936](https://github.com/optimizely/javascript-sdk/pull/936)) + +## [5.3.3] - Jun 06, 2024 + +### Changed + +- queueMicroTask fallback addition for embedded environments / unsupported platforms ([#933](https://github.com/optimizely/javascript-sdk/pull/933)) + +## [5.3.2] - May 20, 2024 + +### Changed + +- Added public facing API for ODP integration information ([#930](https://github.com/optimizely/javascript-sdk/pull/930)) + + +## [5.3.1] - May 20, 2024 + +### Changed +- Fix Memory Leak: Closed http request after getting response to release memory immediately (node) ([#927](https://github.com/optimizely/javascript-sdk/pull/927)) + +## [5.3.1-rc.1] - May 13, 2024 + +### Changed +- Fix Memory Leak: Closed http request after getting response to release memory immediately (node) ([#927](https://github.com/optimizely/javascript-sdk/pull/927)) + +## [5.3.0] - April 8, 2024 + +### Changed +- Refactor: ODP corrections [#920](https://github.com/optimizely/javascript-sdk/pull/920) including + - ODPManager should not be running and scheduling timer if ODP is not integrated to the project (which causes memory leak if one sdk instance is created per request) + - CreateUserContext should work even when called before the datafile is downloaded and should send the `identify` ODP events after datafile download completes + - Other automatic odp events (vuid registration, client initialized) should also be sent after datafile is available and should not be dropped if batching is disabled. + - [see PR for more] + + +## [5.2.1] - March 25, 2024 + +### Bug fixes +- Fix: empty segments collection is valid ([#916](https://github.com/optimizely/javascript-sdk/pull/916)) +- Update vulnerable dependencies ([#918](https://github.com/optimizely/javascript-sdk/pull/918)) + +## [5.2.0] - March 18, 2024 + +### New Features +- Add `persistentCacheProvider` option to `createInstance` to allow providing custom persistent cache implementation in react native ([#914](https://github.com/optimizely/javascript-sdk/pull/914)) + +## [5.1.0] - March 1, 2024 + +### New Features +- Add explicit entry points for node, browser and react_native, allowing imports like `import optimizelySdk from '@optimizely/optimizely-sdk/node'`, `import optimizelySdk from '@optimizely/optimizely-sdk/browser'`, `import optimizelySdk from '@optimizely/optimizely-sdk/react_native'` ([#905](https://github.com/optimizely/javascript-sdk/pull/905)) + +### Changed +- Log an error in DatafileManager when datafile fetch fails ([#904](https://github.com/optimizely/javascript-sdk/pull/904)) + +## [5.0.1] - February 20, 2024 + +### Bug fixes +- Improved conditional ODP instantiation when `odpOptions.disabled: true` is used ([#902](https://github.com/optimizely/javascript-sdk/pull/902)) + +### Changed +- Updated Dependabot alerts ([#896](https://github.com/optimizely/javascript-sdk/pull/896)) +- Updated several devDependencies ([#898](https://github.com/optimizely/javascript-sdk/pull/898), [#900](https://github.com/optimizely/javascript-sdk/pull/900), [#901](https://github.com/optimizely/javascript-sdk/pull/901)) + + +## [5.0.0] - January 19, 2024 + +### New Features + +The 5.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) ([#765](https://github.com/optimizely/javascript-sdk/pull/765), [#775](https://github.com/optimizely/javascript-sdk/pull/775), [#776](https://github.com/optimizely/javascript-sdk/pull/776), [#777](https://github.com/optimizely/javascript-sdk/pull/777), [#778](https://github.com/optimizely/javascript-sdk/pull/778), [#786](https://github.com/optimizely/javascript-sdk/pull/786), [#789](https://github.com/optimizely/javascript-sdk/pull/789), [#790](https://github.com/optimizely/javascript-sdk/pull/790), [#797](https://github.com/optimizely/javascript-sdk/pull/797), [#799](https://github.com/optimizely/javascript-sdk/pull/799), [#808](https://github.com/optimizely/javascript-sdk/pull/808)). + +You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool. + +With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Customer Success Manager. + +This version includes the following changes: + +- New API added to `OptimizelyUserContext`: + + - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays. + + - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities. + +- New APIs added to `OptimizelyClient`: + + - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. + + - `createUserContext()` with anonymous user IDs: user-contexts can be created without a userId. The SDK will create and use a persistent `VUID` specific to a device when userId is not provided. + +For details, refer to our documentation pages: + +- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) + +- [Client SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-client-side-sdks) + +- [Initialize JavaScript SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-javascript-aat) + +- [OptimizelyUserContext JavaScript SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-javascript-aat) + +- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-javascript) + +- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-javascript) + +Additionally, a handful of major package updates are also included in this release including `murmurhash`, `uuid`, and others. For more information, check out the **Breaking Changes** section below. ([#892](https://github.com/optimizely/javascript-sdk/pull/892), [#762](https://github.com/optimizely/javascript-sdk/pull/762)) + +### Breaking Changes +- `ODPManager` in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most `ODPManager` functions will be ignored. If needed, `ODPManager` can be disabled when `OptimizelyClient` is instantiated. +- Updated `murmurhash` dependency to version `2.0.1`. +- Updated `uuid` dependency to version `9.0.1`. +- Dropped support for the following browser versions. + - All versions of Microsof Internet Explorer. + - Chrome versions earlier than `102.0`. + - Microsoft Edge versions earlier than `84.0`. + - Firefox versions earlier than `91.0`. + - Opera versions earlier than `76.0`. + - Safari versions earlier than `13.0`. +- Dropped support for Node JS versions earlier than `16`. + +## Changed +- Updated `createUserContext`'s `userId` parameter to be optional due to the Browser variation's use of the new `vuid` field. Note: The Node variation of the SDK does **not** use the new `vuid` field and you should pass in a `userId` when within the context of the Node variant. + + +## [4.10.0] - October 11, 2023 + +### New Features +- Add support for configurable closing event dispatcher, and dispatching events using sendBeacon in the browser on instance close ([#876](https://github.com/optimizely/javascript-sdk/pull/876), [#874](https://github.com/optimizely/javascript-sdk/pull/874), [#873](https://github.com/optimizely/javascript-sdk/pull/873)) + +## [5.0.0-beta5] - September 1, 2023 + +### Changed +- Exported logging related types and values from the package entrypoint ([#858](https://github.com/optimizely/javascript-sdk/pull/858)) +- Removed /lib directory from the published pacakage ([#862](https://github.com/optimizely/javascript-sdk/pull/862)) + +## [5.0.0-beta4] - August 22, 2023 + +### New Features +- Added support for configurable user agent parser for ODP ([#854](https://github.com/optimizely/javascript-sdk/pull/854)) + +### Bug fixes +- Fixed typescript compilation failure due to missing types ([#856](https://github.com/optimizely/javascript-sdk/pull/856)) + +## [5.0.0-beta3] - August 18, 2023 + +### Bug fixes +- Fixed odp event sending not working for Europe and Asia-Pacific regions ([#852](https://github.com/optimizely/javascript-sdk/pull/852)) + +### Changed +- Remove 1 second polling floor to allow datafile polling at any frequency but for intervals under 30 seconds, log a warning ([#841](https://github.com/optimizely/javascript-sdk/pull/841)). + +## [5.0.0-beta2] - July 19, 2023 + +### Performance Improvements +- Improved OptimizelyConfig class instantiation performance from O(n^2) to O(n) where n = number of feature flags ([#828](https://github.com/optimizely/javascript-sdk/pull/828)) + +### Bug fixes + +- Fixed ODP config update issue on datafile update ([#830](https://github.com/optimizely/javascript-sdk/pull/830)) + +## [4.9.4] - June 8, 2023 + +### Performance Improvements +- Improve OptimizelyConfig class instantiation performance from O(n^2) to O(n) where n = number of feature flags ([#829](https://github.com/optimizely/javascript-sdk/pull/829)) + +## 5.0.0-beta +May 4, 2023 + +### New Features + +The 5.0.0-beta release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) ([#765](https://github.com/optimizely/javascript-sdk/pull/765), [#775](https://github.com/optimizely/javascript-sdk/pull/775), [#776](https://github.com/optimizely/javascript-sdk/pull/776), [#777](https://github.com/optimizely/javascript-sdk/pull/777), [#778](https://github.com/optimizely/javascript-sdk/pull/778), [#786](https://github.com/optimizely/javascript-sdk/pull/786), [#789](https://github.com/optimizely/javascript-sdk/pull/789), [#790](https://github.com/optimizely/javascript-sdk/pull/790), [#797](https://github.com/optimizely/javascript-sdk/pull/797), [#799](https://github.com/optimizely/javascript-sdk/pull/799), [#808](https://github.com/optimizely/javascript-sdk/pull/808)). + +You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool. + +With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Customer Success Manager. + +This version includes the following changes: + +- New API added to `OptimizelyUserContext`: + + - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays. + + - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities. + +- New APIs added to `OptimizelyClient`: + + - `sendOdpEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. + + - `createUserContext()` with anonymous user IDs: user-contexts can be created without a userId. The SDK will create and use a persistent `VUID` specific to a device when userId is not provided. + +For details, refer to our documentation pages: + +- [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) + +- [Client SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-client-side-sdks) + +- [Initialize JavaScript SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-javascript-aat) + +- [OptimizelyUserContext JavaScript SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-javascript-aat) + +- [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-javascript) + +- [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-javascript) + +Additionally, a handful of major package updates are also included in this release including `murmurhash`, `uuid`, and others. For more information, check out the **Breaking Changes** section below. ([#762](https://github.com/optimizely/javascript-sdk/pull/762)) + +### Breaking Changes +- `ODPManager` in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most `ODPManager` functions will be ignored. If needed, `ODPManager` can be disabled when `OptimizelyClient` is instantiated. +- Updated `murmurhash` dependency to version `2.0.1`. +- Updated `uuid` dependency to version `8.3.2`. +- Dropped support for the following browser versions. + - All versions of Microsof Internet Explorer. + - Chrome versions earlier than `102.0`. + - Microsoft Edge versions earlier than `84.0`. + - Firefox versions earlier than `91.0`. + - Opera versions earlier than `76.0`. + - Safari versions earlier than `13.0`. +- Dropped support for Node JS versions earlier than `14`. + +## Changed +- Updated `createUserContext`'s `userId` parameter to be optional due to the Browser variation's use of the new `vuid` field. Note: The Node variation of the SDK does **not** use the new `vuid` field and you should pass in a `userId` when within the context of the Node variant. + +## [4.9.3] - March 17, 2023 + +### Changed +- Updated README.md and package.json files to reflect that this SDK supports both Optimizely Feature Experimentation and Optimizely Full Stack ([#803](https://github.com/optimizely/javascript-sdk/pull/803)). + +## [4.9.2] - June 27, 2022 + +### Changed +- Add package.json script for running Karma tests locally using Chrome ([#651](https://github.com/optimizely/javascript-sdk/pull/651)). +- Replaced explicit typescript typings with auto generated ones ([#745](https://github.com/optimizely/javascript-sdk/pull/745)). +- Integrated code from `utils` package into `optimizely-sdk` ([#749](https://github.com/optimizely/javascript-sdk/pull/749)). +- Added ODP Segments support in Audience Evaluation ([#765](https://github.com/optimizely/javascript-sdk/pull/765)). + +## [4.9.1] - January 18, 2022 + +### Bug fixes +- Fixed typescript compilation issue introduced by `4.9.0` ([#733](https://github.com/optimizely/javascript-sdk/pull/733)) + +## [4.9.0] - January 14, 2022 + +### New Features +- Add a set of new APIs for overriding and managing user-level flag, experiment and delivery rule decisions. These methods can be used for QA and automated testing purposes. They are an extension of the OptimizelyUserContext interface ([#705](https://github.com/optimizely/javascript-sdk/pull/705), [#727](https://github.com/optimizely/javascript-sdk/pull/727), [#729](https://github.com/optimizely/javascript-sdk/pull/729), [#730](https://github.com/optimizely/javascript-sdk/pull/730)): + - setForcedDecision + - getForcedDecision + - removeForcedDecision + - removeAllForcedDecisions + +- For details, refer to our documentation pages: [OptimizelyUserContext](https://docs.developers.optimizely.com/full-stack/v4.0/docs/optimizelyusercontext-javascript-node) and [Forced Decision methods](https://docs.developers.optimizely.com/full-stack/v4.0/docs/forced-decision-methods-javascript-node). + +## [4.8.0] - November 29, 2021 + +### New Features +- Added a Lite bundle which does not include Data file manager and Event Processor packages. This reduces the bundle size up to 20% and is helpful for some platforms (such as edge service providers) that do not need extended functionality offered by these packages. +- Removed Client engine validation in the SDK to allow tracking events from new clients without modifying SDK code. + +### Performance Improvements +- Reduced SDK client initialization time by removing `OptimizelyConfig` creation from initialization. The `OptimizelyConfig` object is now created on the first call to `getOptimizelyConfig` API. +- Made Improvements to logging mechanism. The SDK no longer concatenates and formats messages which do not qualify for the log level set by the user. + +### Changed +- Updated `json-schema` package version to `0.4.0` to fix a high-severity vulnerability ([Prototype Pollution](https://snyk.io/vuln/SNYK-JS-JSONSCHEMA-1920922)). + +## [4.8.0-beta.2] - November 1, 2021 + +### New Features +- Removed Client engine validation in the SDK to allow tracking events from new clients without modifying SDK code. + +## [4.8.0-beta] - October 18, 2021 + +### New Features +- Added a Lite bundle which does not include Data file manager and Event Processor packages. This reduces the bundle size up to 20% and is helpful for some platforms (such as edge service providers) that do not need extended functionality offered by these packages. + +### Performance Improvements +- Reduced SDK client initialization time by removing `OptimizelyConfig` creation from initialization. The `OptimizelyConfig` object is now created on the first call to `getOptimizelyConfig` API. +- Made Improvements to logging mechanism. The SDK now no longer concatenates and formats messages which do not qualify for the log level set by the user. + +## [4.7.0] - September 15, 2021 + +### New Features +- Added new public properties to `OptimizelyConfig`. ([#683](https://github.com/optimizely/javascript-sdk/pull/683), [#698](https://github.com/optimizely/javascript-sdk/pull/698)) + - sdkKey + - environmentKey + - attributes + - audiences + - events + - experimentRules and deliveryRules to `OptimizelyFeature` + - audiences to `OptimizelyExperiment` +- For details, refer to our documentation page: + - Node version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/optimizelyconfig-javascript-node](https://docs.developers.optimizely.com/full-stack/v4.0/docs/optimizelyconfig-javascript-node). + - Browser version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/optimizelyconfig-javascript](https://docs.developers.optimizely.com/full-stack/v4.0/docs/optimizelyconfig-javascript). + +### Bug fixes +- Followed experimentIds order of experiments inside featuresMap of OptimizelyConfig ([#701](https://github.com/optimizely/javascript-sdk/pull/701)) + +### Deprecated + +- `OptimizelyFeature.experimentsMap` of `OptimizelyConfig` is deprecated as of this release. Please use `OptimizelyFeature.experimentRules` and `OptimizelyFeature.deliveryRules` ([#698](https://github.com/optimizely/javascript-sdk/pull/698)) + +## [4.6.2] - July 15, 2021 + +### Bug fixes +- Fixed incorrect impression event payload in projects containing multiple flags with dublicate key rules ([#690](https://github.com/optimizely/javascript-sdk/pull/690)) + +## [4.6.1] - July 8, 2021 + +### Bug fixes +- Bumped `event-processor` packages to version `0.8.2` +- Fixed serving incorrect variation issue in projects containing multiple flags with same key rules ([#687](https://github.com/optimizely/javascript-sdk/pull/687)) + +## [4.6.0] - May 27, 2021 + +### New Features +- Added support for multiple concurrent prioritized experiments per flag ([#664](https://github.com/optimizely/javascript-sdk/pull/664)) + +### Bug fixes +- Fixed the issue of forced-variation and whitelist not working properly with exclusion group experiments ([#664](https://github.com/optimizely/javascript-sdk/pull/664)) +- Bumped `datafile-manager` and `event-processor` packages to version `0.8.1`. + +## [4.5.1] - March 2, 2021 + +### Bug fixes +- Refactored TypeScript type definitions to have `OptimizelyUserContext` and `OptimizelyDecision` imported from `shared_types` to provide isolation from internal modules ([#655](https://github.com/optimizely/javascript-sdk/pull/655)) + +## [4.5.0] - February 17, 2021 + +### New Features + +- Introducing a new primary interface for retrieving feature flag status, configuration and associated experiment decisions for users ([#632](https://github.com/optimizely/javascript-sdk/pull/632), [#634](https://github.com/optimizely/javascript-sdk/pull/634), [#635](https://github.com/optimizely/javascript-sdk/pull/635), [#636](https://github.com/optimizely/javascript-sdk/pull/636), [#640](https://github.com/optimizely/javascript-sdk/pull/640), [#642](https://github.com/optimizely/javascript-sdk/pull/642), [#643](https://github.com/optimizely/javascript-sdk/pull/643), [#644](https://github.com/optimizely/javascript-sdk/pull/644), [#647](https://github.com/optimizely/javascript-sdk/pull/647), [#648](https://github.com/optimizely/javascript-sdk/pull/648)). The new `OptimizelyUserContext` class is instantiated with `createUserContext` and exposes the following APIs to get `OptimizelyDecision`: + + - setAttribute + - decide + - decideAll + - decideForKeys + - trackEvent + +- For details, refer to our documentation page: + - browser version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk). + - Node version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk). + +## [4.5.0-beta] - February 10, 2021 + +### New Features + +- Introducing a new primary interface for retrieving feature flag status, configuration and associated experiment decisions for users ([#632](https://github.com/optimizely/javascript-sdk/pull/632), [#634](https://github.com/optimizely/javascript-sdk/pull/634), [#635](https://github.com/optimizely/javascript-sdk/pull/635), [#636](https://github.com/optimizely/javascript-sdk/pull/636), [#640](https://github.com/optimizely/javascript-sdk/pull/640), [#642](https://github.com/optimizely/javascript-sdk/pull/642), [#643](https://github.com/optimizely/javascript-sdk/pull/643), [#644](https://github.com/optimizely/javascript-sdk/pull/644), [#647](https://github.com/optimizely/javascript-sdk/pull/647), [#648](https://github.com/optimizely/javascript-sdk/pull/648)). The new `OptimizelyUserContext` class is instantiated with `createUserContext` and exposes the following APIs to get `OptimizelyDecision`: + + - setAttribute + - decide + - decideAll + - decideForKeys + - trackEvent + +- For details, refer to our documentation page: + - browser version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-sdk). + - Node version: [https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk](https://docs.developers.optimizely.com/full-stack/v4.0/docs/javascript-node-sdk). + +## [4.4.3] - November 23, 2020 + +### Bug fixes +- Refactored TypeScript type definitions to have `OptimizelyOptions` imported from `shared_types` to provide isolation from internal modules ([#629](https://github.com/optimizely/javascript-sdk/pull/629)) + +## [4.4.2] - November 19, 2020 + +### Bug fixes + +- In `Optimizely` class, use `any` type when assigning the return value of `setTimeout`. This is to allow it to type check regardless of whether it uses the browser or Node version of `setTimeout` ([PR #623](https://github.com/optimizely/javascript-sdk/pull/623)), ([Issue #622](https://github.com/optimizely/javascript-sdk/issues/622)) +- Allowed to pass string type `logLevel` to `createInstance`. ([PR #627](https://github.com/optimizely/javascript-sdk/pull/627)), ([Issue #614](https://github.com/optimizely/javascript-sdk/issues/614)) +- Excluded `suppressImplicitAnyIndexErrors` from TSconfig and resolved reported TS compiler issues ([PR #616](https://github.com/optimizely/javascript-sdk/pull/616)), ([Issue #613](https://github.com/optimizely/javascript-sdk/issues/613)) +- Refactored TypeScript type definitions to only import from `shared_types` to provide isolation from internal modules ([#625](https://github.com/optimizely/javascript-sdk/pull/625)) + +### New Features + +- Added `enabled` field to decision metadata structure to support upcoming application-controlled introduction of tracking for non-experiment Flag decisions ([#619](https://github.com/optimizely/javascript-sdk/pull/619)) + +## [4.4.1] - November 5, 2020 + +### Bug fixes + +- Allowed using `--isolatedModules` flag in TSConfig file by fixing exports in event processor ([#610](https://github.com/optimizely/javascript-sdk/pull/610)) +- Fixed strictNullChecks type errors ([#611](https://github.com/optimizely/javascript-sdk/pull/611)) + +## [4.4.0] - November 2, 2020 + +### New Features + +- Added support sending impression events every time a decision is made ([#599](https://github.com/optimizely/javascript-sdk/pull/599)) + +## [4.3.4] - October 8, 2020 + +### Bug fixes +- The prior version (4.3.3) was erroneously published with the wrong content. This version contains up-to-date content, with no new changes. + +## [4.3.3] - October 7, 2020 + +### Bug fixes +- Exported `OptimizelyVariable`, `OptimizelyVariation`, `OptimizelyExperiment`, `OptimizelyFeature`, `UserProfileService`, and `UserProfile` types from TypeScript type definitions ([#594](https://github.com/optimizely/javascript-sdk/pull/594)) + +## [4.3.2] - October 6, 2020 + +### Bug fixes + +- Fixed return type of `getAllFeatureVariables` method and `dispatchEvent ` method signature of `EventDispatcher` interface in TypeScript type definitions ([#576](https://github.com/optimizely/javascript-sdk/pull/576)) +- Don't log an error message when initialized with `sdkKey`, but no `datafile` ([#589](https://github.com/optimizely/javascript-sdk/pull/589)) + +## [4.3.1] - October 5, 2020 + +### Bug fixes + +- Exported `OptimizelyConfig` and `UserAttributes` type in TypeScript type definitions ([#587](https://github.com/optimizely/javascript-sdk/pull/587)) + +## [4.3.0] - October 1, 2020 + +### New Features + +- Added support for version audience evaluation ([#517](https://github.com/optimizely/javascript-sdk/pull/571)) +- Add datafile accessor ([#564](https://github.com/optimizely/javascript-sdk/pull/564)) + +## [4.2.1] - August 10, 2020 + +### Bug fixes + - Remove incorrect warning about invalid variation ID when user not bucketed into experiment or feature rollout ([#549](https://github.com/optimizely/javascript-sdk/pull/549)) + +## [4.2.0] - July 31, 2020 + +### New Features + + - Better offline support in React Native apps: + - Persist downloaded datafiles in local storage for use in subsequent SDK initializations ([#430](https://github.com/optimizely/javascript-sdk/pull/430)) + - Persist pending impression & conversion events in local storage ([#517](https://github.com/optimizely/javascript-sdk/pull/517), [#532](https://github.com/optimizely/javascript-sdk/pull/532)) + +### Bug fixes + + - Fixed log messages for Targeted Rollouts ([#515](https://github.com/optimizely/javascript-sdk/pull/515)) + +## [4.1.0] - July 7, 2020 + +### New Features + +- Added support for JSON feature variables: new methods `getFeatureVariableJSON` and `getAllFeatureVariables` ([#467](https://github.com/optimizely/javascript-sdk/pull/467), [#470](https://github.com/optimizely/javascript-sdk/pull/470)) +- Added support for authenticated datafiles when running in Node.js. Pass `datafileAccessToken` within `datafileOptions` to request an authenticated datafile using the token ([#498](https://github.com/optimizely/javascript-sdk/pull/498), [#502](https://github.com/optimizely/javascript-sdk/pull/502)): + ```js + const optimizelySDK = require('@optimizely/optimizely-sdk'); + var optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '<Your SDK key>', + datafileOptions: { + datafileAccessToken: '<Your datafile access token>', + } + }); + ``` + +### Bug fixes + +- Fixed audience evaluation log level: changed from `INFO` to `DEBUG` ([#496](https://github.com/optimizely/javascript-sdk/pull/496)) +- Temporarily disabled React Native FSC tests ([#514](https://github.com/optimizely/javascript-sdk/pull/514)) +- Changed `getFeatureVariableJson` to `getFeatureVariableJSON` ([#516](https://github.com/optimizely/javascript-sdk/pull/516)) + +## [4.1.0-beta] - June 16, 2020 + +### New Features + +- Added support for JSON feature variables: new methods `getFeatureVariableJSON` and `getAllFeatureVariables` ([#467](https://github.com/optimizely/javascript-sdk/pull/467), [#470](https://github.com/optimizely/javascript-sdk/pull/470)) +- Added support for authenticated datafiles when running in Node.js. Pass `datafileAccessToken` within `datafileOptions` to request an authenticated datafile using the token ([#498](https://github.com/optimizely/javascript-sdk/pull/498), [#502](https://github.com/optimizely/javascript-sdk/pull/502)): + ```js + const optimizelySDK = require('@optimizely/optimizely-sdk'); + var optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '<Your SDK key>', + datafileOptions: { + datafileAccessToken: '<Your datafile access token>', + } + }); + ``` + +### Bug fixes + +- Fixed audience evaluation log level: changed from `INFO` to `DEBUG` ([#496](https://github.com/optimizely/javascript-sdk/pull/496)) + +## [4.0.0] - April 30, 2020 + +### New Features + +- Removed lodash dependency +- ES module entry point for the browser - `"module"` property of `package.json` points to `dist/optimizely.browser.es.min.js` ([#445](https://github.com/optimizely/javascript-sdk/pull/445)) + +### Breaking Changes + +- Removed `Promise` polyfill from browser entry point ([417](https://github.com/optimizely/javascript-sdk/pull/417)). +- Changed functionality of JSON schema validation in all entry points ([442](https://github.com/optimizely/javascript-sdk/pull/442)). + - Previously, `skipJSONValidation` flag was used by the user to specify whether the JSON object should be validated. + - Now, `skipJSONValidation` has been removed entirely from all entry points. Instead, a user will need to import `jsonSchemaValidator` from `@optimizely/optimizely-sdk/dist/optimizely.json_schema_validator.min.js` and pass it to `createInstance` to perform validation as shown below: + ```js + const optimizelySDK = require('@optimizely/optimizely-sdk'); + const jsonSchemaValidator = require('@optimizely/optimizely-sdk/dist/optimizely.json_schema_validator.min'); + + // Require JSON schema validation for the datafile + var optimizelyClientInstance = optimizely.createInstance({ + datafile: datafile, + jsonSchemaValidator: jsonSchemaValidator, + }); + ``` +- Dropped support for Node.js version <8 ([#456](https://github.com/optimizely/javascript-sdk/pull/456)) + +### Bug fixes + +- Changed `track()` to log a warning instead of an error when the event isn't in the datafile ([#418](https://github.com/optimizely/javascript-sdk/pull/418)) +- Fixed return type for `close` method in TypeScript type definitions ([#410](https://github.com/optimizely/javascript-sdk/pull/410)) +- Node.js datafile manager uses gzip,deflate compression for requests ([#456](https://github.com/optimizely/javascript-sdk/pull/456)) + +## [4.0.0-rc.2] - April 24, 2020 + +### Bug fixes + +- Allow multiple instances to be created from the same datafile object ([#462](https://github.com/optimizely/javascript-sdk/pull/462)) + +## [4.0.0-rc.1] - April 17, 2020 + +### New Features + +- ES module entry point for the browser - `"module"` property of `package.json` points to `dist/optimizely.browser.es.min.js` ([#445](https://github.com/optimizely/javascript-sdk/pull/445)) + +### Breaking Changes: + +- Dropped support for Node.js version <8 ([#456](https://github.com/optimizely/javascript-sdk/pull/456)) + +### Bug fixes + +- Node.js datafile manager uses gzip,deflate compression for requests ([#456](https://github.com/optimizely/javascript-sdk/pull/456)) + +## [4.0.0-alpha.1] - March 4, 2020 + +### Breaking Changes: + +- Removed `Promise` polyfill from browser entry point ([417](https://github.com/optimizely/javascript-sdk/pull/417)). +- Changed functionality of JSON schema validation in all entry points ([442](https://github.com/optimizely/javascript-sdk/pull/442)). + - Previously, `skipJSONValidation` flag was used by the user to specify whether the JSON object should be validated. + - Now, `skipJSONValidation` has been removed entirely from all entry points. Instead, a user will need to import `jsonSchemaValidator` from `@optimizely/optimizely-sdk/dist/optimizely.json_schema_validator.min.js` and pass it to `createInstance` to perform validation as shown below: + + ```js + const optimizelySDK = require('@optimizely/optimizely-sdk'); + const jsonSchemaValidator = require('@optimizely/optimizely-sdk/dist/optimizely.json_schema_validator.min'); + + // Require JSON schema validation for the datafile + var optimizelyClientInstance = optimizely.createInstance({ + datafile: datafile, + jsonSchemaValidator: jsonSchemaValidator, + }); + ``` + +## [3.6.0-alpha.1] - March 4, 2020 + +### New Features + +- Changed `track()` to log a warning instead of an error when the event isn't in the datafile ([#418](https://github.com/optimizely/javascript-sdk/pull/418)) + +## [3.5.0] - February 20th, 2020 + +### Bug fixes + +- Fixed default event dispatcher not used in React Native entry point ([#383](https://github.com/optimizely/javascript-sdk/pull/383)) +- Fixed errors in `getOptimizelyConfig` TypeScript type definitions ([#406](https://github.com/optimizely/javascript-sdk/pull/406)) + +### New Features + +- Promise returned from `close` tracks the state of in-flight event dispatcher requests ([#404](https://github.com/optimizely/javascript-sdk/pull/404)) + +## [3.4.1] - January 28th, 2020 + +### Bug fixes + +- Added `getOptimizelyConfig` and related types to TypeScript type definitions([#390](https://github.com/optimizely/javascript-sdk/pull/390)). + +## [3.4.0] - January 21th, 2020 + +### Bug fixes + +- Fixed incorrect payload for decision notification triggered by calling getVariation on a feature test in a mutex group([#375](https://github.com/optimizely/javascript-sdk/pull/375)). + +### New Features + +- Added a new API to get project configuration static data. + - Call `getOptimizelyConfig()` to get a snapshot of project configuration static data. + - It returns an `OptimizelyConfig` instance which includes a datafile revision number, all experiments, and feature flags mapped by their key values. + - Added caching for `getOptimizelyConfig` - `OptimizelyConfig` object will be cached and reused for the lifetime of the datafile. + - For details, refer to our documentation page: [https://docs.developers.optimizely.com/full-stack/docs/optimizelyconfig-javascript-node](https://docs.developers.optimizely.com/full-stack/docs/optimizelyconfig-javascript-node). + +### Removed Features + +- Removed support for `'launched'` experiment status + - Previously, experiments with status `'running'` or `'launched'` would return non-`null` variations from `activate` and `getVariation`, and generate impression events from `activate` + - Now, only `'running'` experiments will return non-`null` variations and generate impressions + +## [3.4.0-beta] - December 18th, 2019 + +### Bug fixes + +- Fixed incorrect payload for decision notification triggered by calling getVariation on a feature test in a mutex group([#375](https://github.com/optimizely/javascript-sdk/pull/375)) + +### New Features + +- Added a new API to get a project configuration static data. + - Call `getOptimizelyConfig()` to get a snapshot copy of project configuration static data. + - It returns an `OptimizelyConfig` instance which includes a datafile revision number, all experiments, and feature flags mapped by their key values. + - For details, refer to a documention page: https://docs.developers.optimizely.com/full-stack/docs/optimizelyconfig-javascript-node + +## [3.3.2] - November 14th, 2019 + +### Bug fixes + +- Fixed error message that was being logged when a user was bucketed into empty space in an experiment or a mutual exclusion group. This is not an error. With the fix, the message indicates that the user was not included in any experiment ([#366](https://github.com/optimizely/javascript-sdk/pull/366)). + +## [3.3.1] - October 25th, 2019 + +### Bug fixes + +- Fixed full screen error dialog appearing in local development for React Native apps when using the default logger. We now provide a default logger for React Native that does not call `console.error`. + +## [3.3.0] - September 25th, 2019 + +### New Features + +- Added support for event batching via the event processor. + - Events generated by methods like `activate`, `track`, and `isFeatureEnabled` will be held in a queue until the configured batch size is reached, or the configured flush interval has elapsed. Then, they will be combined into a request and sent to the event dispatcher. + - To configure event batching, include the `eventBatchSize` and `eventFlushInterval` number properties in the object you pass to `createInstance`. + - Event batching is enabled by default. `eventBatchSize` defaults to `10`. `eventFlushInterval` defaults to `30000` in Node and `1000` in browsers. +- Added `localStorage` mitigation against lost events in the browser + - When event requests are dispatched, they are written to `localStorage`, and when a response is received, they are removed from `localStorage`. + - When the SDK is initialized for the first time in the browser, if any requests remain in `localStorage`, they will be sent, and removed from `localStorage` when a response is received. +- Updated the `close` method to return a `Promise` representing the process of closing the instance. When `close` is called, any events waiting to be sent as part of a batched event request will be immediately batched and sent to the event dispatcher. + - If any such requests were sent to the event dispatcher, `close` returns a `Promise` that fulfills after the event dispatcher calls the response callback for each request. Otherwise, `close` returns an immediately-fulfilled `Promise`. + - The `Promise` returned from `close` is fulfilled with a result object containing `success` (boolean) and `reason` (string, only when success is `false`) properties. In the result object, `success` is `true` if all events in the queue at the time close was called were combined into requests, sent to the event dispatcher, and the event dispatcher called the callbacks for each request. `success` is false if an unexpected error was encountered during the close process. +- Added non-typed `getFeatureVariable` method ([#298](https://github.com/optimizely/javascript-sdk/pull/298)) as a more idiomatic approach to getting values of feature variables. + - Typed `getFeatureVariable` methods will still be available for use. + +## [3.3.0-beta] - August 21th, 2019 + +### New Features + +- Added support for event batching via the event processor. + - Events generated by methods like `activate`, `track`, and `isFeatureEnabled` will be held in a queue until the configured batch size is reached, or the configured flush interval has elapsed. Then, they will be combined into a request and sent to the event dispatcher. + - To configure event batching, include the `eventBatchSize` and `eventFlushInterval` number properties in the object you pass to `createInstance`. + - Event batching is enabled by default. `eventBatchSize` defaults to `10`. `eventFlushInterval` defaults to `30000` in Node and `1000` in browsers. +- Added `localStorage` mitigation against lost events in the browser + - When event requests are dispatched, they are written to `localStorage`, and when a response is received, they are removed from `localStorage`. + - When the SDK is initialized for the first time in the browser, if any requests remain in `localStorage`, they will be sent, and removed from `localStorage` when a response is received. +- Updated the `close` method to return a `Promise` representing the process of closing the instance. When `close` is called, any events waiting to be sent as part of a batched event request will be immediately batched and sent to the event dispatcher. + - If any such requests were sent to the event dispatcher, `close` returns a `Promise` that fulfills after the event dispatcher calls the response callback for each request. Otherwise, `close` returns an immediately-fulfilled `Promise`. + - The `Promise` returned from `close` is fulfilled with a result object containing `success` (boolean) and `reason` (string, only when success is `false`) properties. In the result object, `success` is `true` if all events in the queue at the time close was called were combined into requests, sent to the event dispatcher, and the event dispatcher called the callbacks for each request. `success` is false if an unexpected error was encountered during the close process. +- Added non-typed `getFeatureVariable` method ([#298](https://github.com/optimizely/javascript-sdk/pull/298)) as a more idiomatic approach to getting values of feature variables. + - Typed `getFeatureVariable` methods will still be available for use. + +## [3.2.2] - August 20th, 2019 + +### Bug fixes + +- Dont use pendingEventsDispatcher with user defined eventDispatcher ([#289](https://github.com/optimizely/javascript-sdk/issues/289)) + Note: This was supposed to be released in 3.2.1 but did not make it into the release. +- Updated lodash dependency to ^4.17.11 to address security vulnerabilities ([#296](https://github.com/optimizely/javascript-sdk/issues/296)) + +## [3.2.1] - July 1st, 2019 + +### Changed + +- Updated lodash dependency to ^4.17.11 to address security vulnerabilities ([#296](https://github.com/optimizely/javascript-sdk/issues/296)) + +## [3.2.0] - May 30th, 2019 + +### New Features + +- Added support for automatic datafile management ([#261](https://github.com/optimizely/javascript-sdk/pull/261)), ([#266](https://github.com/optimizely/javascript-sdk/pull/266)), ([#267](https://github.com/optimizely/javascript-sdk/pull/267)), ([#268](https://github.com/optimizely/javascript-sdk/pull/268)), ([#270](https://github.com/optimizely/javascript-sdk/pull/270)), ([#272](https://github.com/optimizely/javascript-sdk/pull/272)) + + - To use automatic datafile management, include `sdkKey` as a string property in the options object you pass to `createInstance`. + - When sdkKey is provided, the SDK instance will download the datafile associated with that sdkKey immediately upon construction. When the download completes, the SDK instance will update itself to use the downloaded datafile. + - Use the `onReady` method to wait until the download is complete and the SDK is ready to use. + - Customize datafile management behavior by passing a `datafileOptions` object within the options you pass to `createInstance`. + - Enable automatic updates by passing `autoUpdate: true`. Periodically (on the provided update interval), the SDK instance will download the datafile and update itself. Use this to ensure that the SDK instance is using a fresh datafile reflecting changes recently made to your experiment or feature configuration. + - Add a notification listener for the `OPTIMIZELY_CONFIG_UPDATE` notification type to be notified when an instance updates its Optimizely config after obtaining a new datafile. + - Stop active downloads and cancel recurring downloads by calling the `close` method + + #### Create an instance with datafile management enabled + + ```js + const optimizely = require('@optimizely/optimizely-sdk'); + const optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '12345', // Provide the sdkKey of your desired environment here + }); + ``` + + #### Use `onReady` to wait until optimizelyClientInstance has a datafile + + ```js + const optimizely = require('@optimizely/optimizely-sdk'); + const optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '12345', + }); + optimizelyClientInstance.onReady().then(() => { + // optimizelyClientInstance is ready to use, with datafile downloaded from the Optimizely CDN + }); + ``` + + #### Enable automatic updates, add notification listener for OPTIMIZELY_CONFIG_UPDATE notification type, and stop automatic updates + + ```js + const optimizely = require('@optimizely/optimizely-sdk'); + const optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '12345', + datafileOptions: { + autoUpdate: true, + updateInterval: 600000, // 10 minutes in milliseconds + }, + }); + optimizelyClientInstance.notificationCenter.addNotificationListener( + optimizely.enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + () => { + // optimizelyClientInstance has updated its Optimizely config + } + ); + // Stop automatic updates - optimizelyClientInstance will use whatever datafile it currently has from now on + optimizelyClientInstance.close(); + ``` + +### Changed + +- Forced variation logic has been moved from the project config module to the decision service. Prefixes for forced-variation-related log messages will reflect this change ([#261](https://github.com/optimizely/javascript-sdk/pull/261)). +- Update TypeScript definitions to account for new methods (`onReady`, `close`) and new properties on object accepted by createInstance (`datafileOptions`, `sdkKey`), ([#263](https://github.com/optimizely/javascript-sdk/pull/263)), ([#278](https://github.com/optimizely/javascript-sdk/pull/278)) +- Allow react-sdk to be passed in as `clientEngine` ([#279](https://github.com/optimizely/javascript-sdk/pull/279)) + +### Bug Fixes: + +- Add logging message for `optimizely.track()` ([#281](https://github.com/optimizely/javascript-sdk/pull/281)) + +## [3.2.0-beta] - May 16th, 2019 + +### Bug Fixes: + +- Clear timeout created in onReady call for timeout promise as soon as project config manager's ready promise fulfills + +### New Features + +- Added 60 second timeout for all datafile requests + +### Changed + +- Updated datafile request polling behavior: + - Start update interval timer immediately after request + - When update interval timer fires during request, wait until request completes, then immediately start next request +- Update TypeScript definitions to account for new methods (`onReady`, `close`) and new properties on object accepted by createInstance (`datafileOptions`, `sdkKey`) + +## [3.2.0-alpha] - April 26nd, 2019 + +### New Features + +- Added support for automatic datafile management + - To use automatic datafile management, include `sdkKey` as a string property in the options object you pass to `createInstance`. + - When sdkKey is provided, the SDK instance will download the datafile associated with that sdkKey immediately upon construction. When the download completes, the SDK instance will update itself to use the downloaded datafile. + - Use the `onReady` method to wait until the download is complete and the SDK is ready to use. + - Customize datafile management behavior by passing a `datafileOptions` object within the options you pass to `createInstance`. + - Enable automatic updates by passing `autoUpdate: true`. Periodically (on the provided update interval), the SDK instance will download the datafile and update itself. Use this to ensure that the SDK instance is using a fresh datafile reflecting changes recently made to your experiment or feature configuration. + - Add a notification listener for the `OPTIMIZELY_CONFIG_UPDATE` notification type to be notified when an instance updates its Optimizely config after obtaining a new datafile. + - Stop active downloads and cancel pending downloads by calling the `close` method + +#### Create an instance with datafile management enabled + +```js +const optimizely = require('@optimizely/optimizely-sdk'); +const optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '12345', // Provide the sdkKey of your desired environment here +}); +``` + +#### Use `onReady` to wait until optimizelyClientInstance has a datafile + +```js +const optimizely = require('@optimizely/optimizely-sdk'); +const optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '12345', +}); +optimizelyClientInstance.onReady().then(() => { + // optimizelyClientInstance is ready to use, with datafile downloaded from the Optimizely CDN +}); +``` + +#### Enable automatic updates, add notification listener for OPTIMIZELY_CONFIG_UPDATE notification type, and stop automatic updates + +```js +const optimizely = require('@optimizely/optimizely-sdk'); +const optimizelyClientInstance = optimizely.createInstance({ + sdkKey: '12345', + datafileOptions: { + autoUpdate: true, + updateInterval: 600000, // 10 minutes in milliseconds + }, +}); +optimizelyClientInstance.notificationCenter.addNotificationListener( + optimizely.enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + () => { + // optimizelyClientInstance has updated its Optimizely config + } +); +// Stop automatic updates - optimizelyClientInstance will use whatever datafile it currently has from now on +optimizelyClientInstance.close(); +``` + +### Changed + +- Forced variation logic has been moved from the project config module to the decision service. Prefixes for forced-variation-related log messages will reflect this change. + +## [3.1.0] - April 22nd, 2019 + +### New Features: + +- Introduced Decision notification listener to be able to record: + - Variation assignments for users activated in an experiment. + - Feature access for users. + - Feature variable value for users. + +### Changed + +- New APIs for setting `logger` and `logLevel` on the optimizelySDK singleton ([#232](https://github.com/optimizely/javascript-sdk/pull/232)) +- `logger` and `logLevel` are now set globally for all instances of Optimizely. If you were passing + different loggers to individual instances of Optimizely, logging behavior may now be different. + +#### Setting a ConsoleLogger + +```js +var optimizelySDK = require('@optimizely/optimizely-sdk'); + +// logger and logLevel are now set on the optimizelySDK singleton +optimizelySDK.setLogger(optimizelySDK.logging.createLogger()); + +// valid levels: 'DEBUG', 'INFO', 'WARN', 'ERROR' +optimizelySDK.setLogLevel('WARN'); +// enums can also be used +optimizelySDK.setLogLevel(optimizelySDK.enums.LOG_LEVEL.ERROR); +``` + +#### Disable logging + +```js +var optimizelySDK = require('@optimizely/optimizely-sdk'); + +optimizelySDK.setLogger(null); +``` + +### Bug Fixes + +- Feature variable APIs now return default variable value when featureEnabled property is false. ([#249](https://github.com/optimizely/javascript-sdk/pull/249)) + +### Deprecated + +- Activate notification listener is deprecated as of this release. Recommendation is to use the new Decision notification listener. Activate notification listener will be removed in the next major release. + +## [3.1.0-beta1] - March 5th, 2019 + +### Changed + +- New APIs for setting `logger` and `logLevel` on the optimizelySDK singleton ([#232](https://github.com/optimizely/javascript-sdk/pull/232)) +- `logger` and `logLevel` are now set globally for all instances of Optimizely. If you were passing + different loggers to individual instances of Optimizely, logging behavior may now be different. + +#### Setting a ConsoleLogger + +```js +var optimizelySDK = require('@optimizely/optimizely-sdk'); + +// logger and logLevel are now set on the optimizelySDK singleton +optimizelySDK.setLogger(optimizelySDK.logging.createLogger()); + +// valid levels: 'DEBUG', 'INFO', 'WARN', 'ERROR' +optimizelySDK.setLogLevel('WARN'); +// enums can also be used +optimizelySDK.setLogLevel(optimizely.enums.LOG_LEVEL.ERROR); +``` + +#### Disable logging + +```js +var optimizelySDK = require('@optimizely/optimizely-sdk'); + +optimizelySDK.setLogger(null); +``` + +## [3.0.1] - February 21, 2019 + +### Changed + +- Expose default `loggers`, `errorHandlers`, `eventDispatcher` and `enums` on top level require. +- `createLogger` and `createNoOpLogger` are available as methods on `optimizelySdk.logging` +- Added `optimizelySdk.errorHandler` +- Added `optimizelySdk.eventDispatcher` +- Added `optimizelySdk.enums` + +## [3.0.0] - February 13, 2019 + +The 3.0 release improves event tracking and supports additional audience targeting functionality. + +### New Features: + +- Event tracking ([#207](https://github.com/optimizely/javascript-sdk/pull/207)): + - The `track` method now dispatches its conversion event _unconditionally_, without first determining whether the user is targeted by a known experiment that uses the event. This may increase outbound network traffic. + - In Optimizely results, conversion events sent by 3.0 SDKs don't explicitly name the experiments and variations that are currently targeted to the user. Instead, conversions are automatically attributed to variations that the user has previously seen, as long as those variations were served via 3.0 SDKs or by other clients capable of automatic attribution, and as long as our backend actually received the impression events for those variations. + - Altogether, this allows you to track conversion events and attribute them to variations even when you don't know all of a user's attribute values, and even if the user's attribute values or the experiment's configuration have changed such that the user is no longer affected by the experiment. As a result, **you may observe an increase in the conversion rate for previously-instrumented events.** If that is undesirable, you can reset the results of previously-running experiments after upgrading to the 3.0 SDK. + - This will also allow you to attribute events to variations from other Optimizely projects in your account, even though those experiments don't appear in the same datafile. + - Note that for results segmentation in Optimizely results, the user attribute values from one event are automatically applied to all other events in the same session, as long as the events in question were actually received by our backend. This behavior was already in place and is not affected by the 3.0 release. +- Support for all types of attribute values, not just strings ([#174](https://github.com/optimizely/javascript-sdk/pull/174), [#204](https://github.com/optimizely/javascript-sdk/pull/204)). + - All values are passed through to notification listeners. + - Strings, booleans, and valid numbers are passed to the event dispatcher and can be used for Optimizely results segmentation. A valid number is a finite number in the inclusive range [-2⁵³, 2⁵³]. + - Strings, booleans, and valid numbers are relevant for audience conditions. +- Support for additional matchers in audience conditions ([#174](https://github.com/optimizely/javascript-sdk/pull/174)): + - An `exists` matcher that passes if the user has a non-null value for the targeted user attribute and fails otherwise. + - A `substring` matcher that resolves if the user has a string value for the targeted attribute. + - `gt` (greater than) and `lt` (less than) matchers that resolve if the user has a valid number value for the targeted attribute. A valid number is a finite number in the inclusive range [-2⁵³, 2⁵³]. + - The original (`exact`) matcher can now be used to target booleans and valid numbers, not just strings. +- Support for A/B tests, feature tests, and feature rollouts whose audiences are combined using `"and"` and `"not"` operators, not just the `"or"` operator ([#175](https://github.com/optimizely/javascript-sdk/pull/175)) +- Updated Pull Request template and commit message guidelines ([#183](https://github.com/optimizely/javascript-sdk/pull/183)). +- Support for sticky bucketing. You can pass an `$opt_experiment_bucket_map` attribute to ensure that the user gets a specific variation ([#179](https://github.com/optimizely/javascript-sdk/pull/179)). +- Support for bucketing IDs when evaluating feature rollouts, not just when evaluating A/B tests and feature tests ([#200](https://github.com/optimizely/javascript-sdk/pull/200)). +- TypeScript declarations ([#199](https://github.com/optimizely/javascript-sdk/pull/199)). + +### Breaking Changes: + +- Conversion events sent by 3.0 SDKs don't explicitly name the experiments and variations that are currently targeted to the user, so these events are unattributed in raw events data export. You must use the new _results_ export to determine the variations to which events have been attributed. +- Previously, notification listeners were only given string-valued user attributes because only strings could be passed into various method calls. That is no longer the case. You may pass non-string attribute values, and if you do, you must update your notification listeners to be able to receive whatever values you pass in ([#174](https://github.com/optimizely/javascript-sdk/pull/174), [#204](https://github.com/optimizely/javascript-sdk/pull/204)). +- Drops `window.optimizelyClient` from the bundled build. Now, `window.optimizelySdk` can be used instead. ([#189](https://github.com/optimizely/javascript-sdk/pull/189)). + +### Bug Fixes: + +- Experiments and features can no longer activate when a negatively targeted attribute has a missing, null, or malformed value ([#174](https://github.com/optimizely/javascript-sdk/pull/174)). + - Audience conditions (except for the new `exists` matcher) no longer resolve to `false` when they fail to find an legitimate value for the targeted user attribute. The result remains `null` (unknown). Therefore, an audience that negates such a condition (using the `"not"` operator) can no longer resolve to `true` unless there is an unrelated branch in the condition tree that itself resolves to `true`. +- `setForcedVariation` now treats an empty variation key as invalid and does not reset the variation ([#185](https://github.com/optimizely/javascript-sdk/pull/185)). +- You can now specify `0` as the `revenue` or `value` for a conversion event when using the `track` method. Previously, `0` was withheld and would not appear in your data export ([#213](https://github.com/optimizely/javascript-sdk/pull/213)). +- The existence of a feature test in an experimentation group no longer causes A/B tests in the same group to activate the same feature ([#194](https://github.com/optimizely/fullstack-sdk-compatibility-suite/pull/194)). + +## [2.3.1] - November 14, 2018 + +### Fixed + +- fix(bundling): Publish the unminified UMD bundle along with the minified one. ([#187](https://github.com/optimizely/javascript-sdk/pull/187)) + +## [2.3.0] - November 14, 2018 + +### New Features + +- Allow sticky bucketing via passing in `attributes.$opt_experiment_bucket_map`, this more easily allows customers to do some async data fetching and ensure a user gets a specific variation. + +``` +const userId = '123' +const expId = '456' +const variationId = '678' +const userAttributes = { + $opt_experiment_bucket_map: { + [expId]: { + variation_id: variationId + } + } +} + +var selectedVariationKey = optimizelyClient.activate('experiment-1', userId, userAttributes); +``` + +## [2.2.0] - September 26, 2018 + +### Fixed + +- Track and activate should not remove null attributes ([#168](https://github.com/optimizely/javascript-sdk/pull/168)) +- Track attributes with valid attribute types ([#166](https://github.com/optimizely/javascript-sdk/pull/166)) +- Prevent SDK from initializing if the datafile version in invalid ([#161](https://github.com/optimizely/javascript-sdk/pull/161)) +- Updating lerna to latest version ([#160](https://github.com/optimizely/javascript-sdk/pull/160)) + +### Changed + +- Change invalid experiment key to debug level ([#165](https://github.com/optimizely/javascript-sdk/pull/165)) + +## [2.1.3] - August 21, 2018 + +### Fixed + +- Send all decisions for the same event in one snapshot. ([#155](https://github.com/optimizely/javascript-sdk/pull/155)) +- Give Node.js consumers the unbundled package ([#133](https://github.com/optimizely/javascript-sdk/pull/133)) + +### Deprecated + +- The UMD build of the SDK now assigns the SDK namespace object to `window.optimizelySdk` rather than to `window.optimizelyClient`. The old name still works, but on its first access a deprecation warning is logged to the console. The alias will be removed in the 3.0.0 release. ([#152](https://github.com/optimizely/javascript-sdk/pull/152)) + +## [2.1.2] - June 25, 2018 + +### Fixed + +- Failure to log success message when event dispatched ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) +- Fix: Don't call success message when event fails to send ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) + +## [2.0.5] - June 25, 2018 + +### Fixed + +- Failure to log success message when event dispatched ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) +- Fix: Don't call success message when event fails to send ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) + +## 2.1.1 + +June 19, 2018 + +- Fix: send impression event for Feature Test with Feature disabled ([#117](https://github.com/optimizely/javascript-sdk/pull/117)) + +## 2.0.4 + +June 19, 2018 + +- Fix: send impression event for Feature Test with Feature disabled ([#117](https://github.com/optimizely/javascript-sdk/pull/117)) + +## 2.1.0 + +May 24, 2018 + +- Introduces support for bot filtering. + +## 2.0.3 + +May 24, 2018 + +- Remove [`request`](https://www.npmjs.com/package/request) dependency ([#98](https://github.com/optimizely/javascript-sdk/pull/98)) +- Add package-lock.json ([#100](https://github.com/optimizely/javascript-sdk/pull/100)) +- Input validation in Activate, Track, and GetVariation methods ([#91](https://github.com/optimizely/javascript-sdk/pull/91) by [@mfahadahmed](https://github.com/mfahadahmed)) + +## 2.0.1 + +April 16th, 2018 + +- Improve browser entry point by pointing to the browser index file instead of the webpack-compiled bundle. ([@DullReferenceException](https://github.com/DullReferenceException) in [#88](https://github.com/optimizely/javascript-sdk/pull/88)) + +## 2.0.0 + +April 11th, 2018 + +This major release of the Optimizely SDK introduces APIs for Feature Management. It also introduces some breaking changes listed below. + +### New Features + +- Introduces the `isFeatureEnabled` API to determine whether to show a feature to a user or not. + +``` +var enabled = optimizelyClient.isFeatureEnabled('my_feature_key', 'user_1', userAttributes); +``` + +- You can also get all the enabled features for the user by calling the following method which returns a list of strings representing the feature keys: + +``` +var enabledFeatures = optimizelyClient.getEnabledFeatures('user_1', userAttributes); +``` + +- Introduces Feature Variables to configure or parameterize your feature. There are four variable types: `Integer`, `String`, `Double`, `Boolean`. + +``` +var stringVariable = optimizelyClient.getFeatureVariableString('my_feature_key', 'string_variable_key', 'user_1'); +var integerVariable = optimizelyClient.getFeatureVariableInteger('my_feature_key', 'integer_variable_key', 'user_1'); +var doubleVariable = optimizelyClient.getFeatureVariableDouble('my_feature_key', 'double_variable_key', 'user_1'); +var booleanVariable = optimizelyClient.getFeatureVariableBoolean('my_feature_key', 'boolean_variable_key', 'user_1'); +``` + +### Breaking changes + +- The `track` API with revenue value as a stand-alone parameter has been removed. The revenue value should be passed in as an entry of the event tags map. The key for the revenue tag is `revenue` and will be treated by Optimizely as the key for analyzing revenue data in results. + +``` +var eventTags = { + 'revenue': 1200 +}; + +optimizelyClient.track('event_key', 'user_id', userAttributes, eventTags); +``` + +- The package name has changed from `optimizely-client-sdk` to `optimizely-sdk` as we have consolidated both Node and JavaScript SDKs into one. + +## 2.0.0-beta1 + +March 29th, 2018 + +This major release of the Optimizely SDK introduces APIs for Feature Management. It also introduces some breaking changes listed below. + +### New Features + +- Introduces the `isFeatureEnabled` API to determine whether to show a feature to a user or not. + +``` +var enabled = optimizelyClient.isFeatureEnabled('my_feature_key', 'user_1', userAttributes); +``` + +- You can also get all the enabled features for the user by calling the following method which returns a list of strings representing the feature keys: + +``` +var enabledFeatures = optimizelyClient.getEnabledFeatures('user_1', userAttributes); +``` + +- Introduces Feature Variables to configure or parameterize your feature. There are four variable types: `Integer`, `String`, `Double`, `Boolean`. + +``` +var stringVariable = optimizelyClient.getFeatureVariableString('my_feature_key', 'string_variable_key', 'user_1'); +var integerVariable = optimizelyClient.getFeatureVariableInteger('my_feature_key', 'integer_variable_key', 'user_1'); +var doubleVariable = optimizelyClient.getFeatureVariableDouble('my_feature_key', 'double_variable_key', 'user_1'); +var booleanVariable = optimizelyClient.getFeatureVariableBoolean('my_feature_key', 'boolean_variable_key', 'user_1'); +``` + +### Breaking changes + +- The `track` API with revenue value as a stand-alone parameter has been removed. The revenue value should be passed in as an entry of the event tags map. The key for the revenue tag is `revenue` and will be treated by Optimizely as the key for analyzing revenue data in results. + +``` +var eventTags = { + 'revenue': 1200 +}; + +optimizelyClient.track('event_key', 'user_id', userAttributes, eventTags); +``` + +- The package name has changed from `optimizely-client-sdk` to `optimizely-sdk` as we have consolidated both Node and JavaScript SDKs into one. + +## 1.6.0 + +- Bump optimizely-server-sdk to version 1.5.0, which includes: + - Implemented IP anonymization. + - Implemented bucketing IDs. + - Implemented notification listeners. + +## 1.5.1 + +- Bump optimizely-server-sdk to version 1.4.2, which includes: + - Bug fix to filter out undefined values in attributes and event tags + - Remove a duplicated test + +## 1.5.0 + +- Bump optimizely-server-sdk to version 1.4.0, which includes: + - Add support for numeric metrics. + - Add getForcedVariation and setForcedVariation methods for client-side variation setting + - Bug fix for filtering out null attribute and event tag values + +## 1.4.3 + +- Default skipJSONValidation to true +- Bump optimizely-server-sdk to version 1.3.3, which includes: + - Removed JSON Schema Validator from Optimizely constructor + - Updated SDK to use new event endpoint + - Minor bug fixes + +## 1.4.2 + +- Minor performance improvements. + +## 1.4.1 + +- Switched to karma/browserstack for cross-browser testing +- Removed es6-promise +- Bump optimizely-server-sdk to version 1.3.1, which includes: + - Minor performance improvements. + +## 1.4.0 + +- Reduce lodash footprint. +- Bump optimizely-server-sdk to version 1.3.0, which includes: + - Introduced user profile service. + - Minor performance and readibility improvements. + +## 1.3.5 + +- Bump optimizely-server-sdk to version 1.2.3, which includes: + - Switched to json-schema library which has a smaller footprint. + - Refactored order of bucketing logic. + - Refactor lodash dependencies. + - Fixed error on validation for objects with undefined values for attributes. + +## 1.3.4 + +- Bump optimizely-server-sdk to version 1.2.2, which includes: + - Use the 'name' field for tracking event tags instead of 'id'. + +## 1.3.3 + +- Include index.js in package.json files to make sure it gets published regardless of node environment. + +## 1.3.2 + +- Bump to 1.3.2 to re-publish to npm + +## 1.3.1 + +- Bump optimizely-server-sdk to version 1.2.1, which includes: + - Gracefully handle empty traffic allocation ranges. + +## 1.3.0 + +- Bump optimizely-server-sdk to version 1.2.0, which includes: + - Introduce support for event tags. + - Add optional eventTags argument to track method signature. + - Removed optional eventValue argument from track method signature. + - Removed optional sessionId argument from activate and track method signatures. + - Allow log level config on createInstance method. + +## 1.2.2 + +- Remove .npmignore to consolidate with .gitignore. +- Add dist and lib directories to "files" in package.json. + +## 1.2.1 + +- Fix webpack build error. + +## 1.2.0 + +- Bump optimizely-server-sdk to version 1.1.0, which includes: + - Add optional sessionId argument to activate and track method signatures. + - Add sessionId and revision to event ticket. + - Add 'Launched' status where user gets bucketed but event is not sent to Optimizely. + +## 1.1.1 + +- Bump to optimizely-server-sdk to version 1.0.1, which includes: + - Fix bug so conversion event is not sent if user is not bucketed into any experiment. + - Bump bluebird version from 3.3.5 to 3.4.6. + - Update event endpoint from p13nlog.dz.optimizely to logx.optimizely. + +## 1.1.0 + +- Add global variable name export for use in non-CommonJS environments +- Remove redundant lodash core dependency to reduce bundle bloat + +## 1.0.0 + +- Introduce support for Full Stack projects in Optimizely X with no breaking changes from previous version. +- Introduce more graceful exception handling in instantiation and core methods. +- Update whitelisting to take precedence over audience condition evaluation. +- Fix bug activating/tracking with attributes not in the datafile. + +## 0.1.4 + +- Add functionality for New Optimizely endpoint. + +## 0.1.3 + +- Add environment detection to event builder so it can distinguish between events sent from node or the browser. + +## 0.1.2 + +- Add CORS param to prevent browsers from logging cors errors in the console when dispatching events. + +## 0.1.1 + +- Remove percentageIncluded field from JSON schema, which is not needed. + +## 0.1.0 + +- Beta release of the Javascript SDK for our Optimizely testing solution diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..15a35bcf9 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,16 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + + # These owners will be the default owners for everything in the repo. +# Unless a later match takes precedence, @global-owner1 and @global-owner2 +# will be requested for review when someone opens a pull request. +* @optimizely/fullstack-devs + + # Order is important; the last matching pattern takes the most precedence. +# When someone opens a pull request that only modifies JS files, only @js-owner +# and not the global owner(s) will be requested for a review. +#*.js @js-owner + + # You can also use email addresses if you prefer. They'll be used to look up +# users just like we do for commit author emails. +#docs/* docs@example.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d8b1dd19..101b4cd35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,21 +5,22 @@ We welcome contributions and feedback! All contributors must sign our [Contribut ## Development process 1. Fork the repository and create your branch from master. -2. Please follow the [commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) for each commit message. +2. Please follow the [commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) for each commit message. 3. Make sure to add tests! -4. Run `npm run lint` to ensure there are no lint errors. -5. `git push` your changes to GitHub. -6. Open a PR from your fork into the master branch of the original repo -7. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`. -8. Open a pull request from `YOUR_NAME/branch_name` to `master`. -9. A repository maintainer will review your pull request and, if all goes well, squash and merge it! +4. Update relevant `CHANGELOG.md` if users should know about this change. +5. Run `npm run lint` to ensure there are no lint errors. +6. `git push` your changes to GitHub. +7. Open a PR from your fork into the master branch of the original repo +8. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`. +9. Open a pull request from `YOUR_NAME/branch_name` to `master`. +10. A repository maintainer will review your pull request and, if all goes well, squash and merge it! ## Pull request acceptance criteria -* **All code must have test coverage.** We use Mocha's chai assertion library and Sinon. Changes in functionality should have accompanying unit tests. Bug fixes should have accompanying regression tests. - * Tests are located in the `tests.js` file. -* Please don't change the `package.json` or `VERSION`. We'll take care of bumping the version when we next release. -* Lint your code with our `npm run lint` before submitting. +- **All code must have test coverage.** We use Mocha's chai assertion library and Sinon. Changes in functionality should have accompanying unit tests. Bug fixes should have accompanying regression tests. + - Tests are located in the `tests.js` file. +- Please don't change the `package.json` or `VERSION`. We'll take care of bumping the version when we next release. +- Lint your code with our `npm run lint` before submitting. ## Style @@ -45,7 +46,7 @@ All contributions are under the CLA mentioned above. For this project, Optimizel * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ - ``` +``` The YEAR above should be the year of the contribution. If work on the file has been done over multiple years, list each year in the section above. Example: Optimizely writes the file and releases it in 2014. No changes are made in 2015. Change made in 2016. YEAR should be “2014, 2016”. diff --git a/LICENSE b/LICENSE index b9f80c5bd..e2d144779 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2016-2017, Optimizely, Inc. and contributors + © Optimizely 2016 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000..8a2173ebf --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,464 @@ +# Migrating v5 to v6 + +This guide will help you migrate your implementation from Optimizely JavaScript SDK v5 to v6. The new version introduces several architectural changes that provide more flexibility and control over SDK components. + +## Table of Contents + +1. [Major Changes](#major-changes) +2. [Client Initialization](#client-initialization) +3. [Project Configuration Management](#project-configuration-management) +4. [Event Processing](#event-processing) +5. [ODP Management](#odp-management) +6. [VUID Management](#vuid-management) +7. [Error Handling](#error-handling) +8. [Logging](#logging) +9. [onReady Promise Behavior](#onready-promise-behavior) +10. [Dispose of Client](#dispose-of-client) +11. [Migration Examples](#migration-examples) + +## Major Changes + +In v6, the SDK architecture has been modularized to give you more control over different components: + +- The monolithic `createInstance` call is now split into multiple factory functions +- Core functionalities (project configuration, event processing, ODP, VUID, logging, and error handling) are now configured through dedicated components created via factory functions, giving you greater flexibility and control in enabling/disabling certain components and allowing optimizing the bundle size for frontend projects. +- Event dispatcher interface has been updated to use Promises +- onReady Promise behavior has changed + +## Client Initialization + +### v5 (Before) + +```javascript +import { createInstance } from '@optimizely/optimizely-sdk'; + +const optimizely = createInstance({ + sdkKey: '<YOUR_SDK_KEY>', + datafile: datafile, // optional + datafileOptions: { + autoUpdate: true, + updateInterval: 300000, // 5 minutes + }, + eventBatchSize: 10, + eventFlushInterval: 1000, + logLevel: LogLevel.DEBUG, + errorHandler: { handleError: (error) => console.error(error) }, + odpOptions: { + disabled: false, + segmentsCacheSize: 100, + segmentsCacheTimeout: 600000, // 10 minutes + } +}); +``` + +### v6 (After) + +```javascript +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + createOdpManager, + createVuidManager, + createLogger, + createErrorNotifier, + DEBUG +} from "@optimizely/optimizely-sdk"; + +// Create a project config manager +const projectConfigManager = createPollingProjectConfigManager({ + sdkKey: '<YOUR_SDK_KEY>', + datafile: datafile, // optional + autoUpdate: true, + updateInterval: 300000, // 5 minutes in milliseconds +}); + +// Create an event processor +const eventProcessor = createBatchEventProcessor({ + batchSize: 10, + flushInterval: 1000, +}); + +// Create an ODP manager +const odpManager = createOdpManager({ + segmentsCacheSize: 100, + segmentsCacheTimeout: 600000, // 10 minutes +}); + +// Create a VUID manager (optional) +const vuidManager = createVuidManager({ + enableVuid: true +}); + +// Create a logger +const logger = createLogger({ + level: DEBUG +}); + +// Create an error notifier +const errorNotifier = createErrorNotifier({ + handleError: (error) => console.error(error) +}); + +// Create the Optimizely client instance +const optimizely = createInstance({ + projectConfigManager, + eventProcessor, + odpManager, + vuidManager, + logger, + errorNotifier +}); +``` + +In case an invalid config is passed to `createInstance`, it returned `null` in v5. In v6, it will throw an error instead of returning null. + +## Project Configuration Management + +In v6, datafile management must be configured by passing in a `projectConfigManager`. Choose either: + +### Polling Project Config Manager + +For automatic datafile updates: + +```javascript +const projectConfigManager = createPollingProjectConfigManager({ + sdkKey: '<YOUR_SDK_KEY>', + datafile: datafileString, // optional + autoUpdate: true, + updateInterval: 60000, // 1 minute + urlTemplate: 'https://custom-cdn.com/datafiles/%s.json' // optional +}); +``` + +### Static Project Config Manager + +When you want to manage datafile updates manually or want to use a fixed datafile: + +```javascript +const projectConfigManager = createStaticProjectConfigManager({ + datafile: datafileString, +}); +``` + +## Event Processing + +In v5, a batch event processor was enabled by default. In v6, an event processor must be instantiated and passed in +explicitly to `createInstance` via the `eventProcessor` option to enable event processing, otherwise no events will +be dispatched. v6 provides two types of event processors: + +### Batch Event Processor + +Queues events and sends them in batches: + +```javascript +const batchEventProcessor = createBatchEventProcessor({ + batchSize: 10, // optional, default is 10 + flushInterval: 1000, // optional, default 1000 for browser +}); +``` + +### Forwarding Event Processor + +Sends events immediately: + +```javascript +const forwardingEventProcessor = createForwardingEventProcessor(); +``` + +### Custom event dispatcher +In both v5 and v6, custom event dispatchers must implement the `EventDispatcher` interface. In v6, the `EventDispatcher` interface has been updated so that the `dispatchEvent` method returns a Promise instead of calling a callback. + +In v5 (Before): + +```javascript +export type EventDispatcherResponse = { + statusCode: number +} + +export type EventDispatcherCallback = (response: EventDispatcherResponse) => void + +export interface EventDispatcher { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void +} +``` + +In v6(After): + +```javascript +export type EventDispatcherResponse = { + statusCode?: number +} + +export interface EventDispatcher { + dispatchEvent(event: LogEvent): Promise<EventDispatcherResponse> +} +``` + +## ODP Management + +In v5, ODP functionality was configured via `odpOptions` and enabled by default. In v6, instantiate an OdpManager and pass to `createInstance` to enable ODP: + +### v5 (Before) + +```javascript +const optimizely = createInstance({ + sdkKey: '<YOUR_SDK_KEY>', + odpOptions: { + disabled: false, + segmentsCacheSize: 100, + segmentsCacheTimeout: 600000, // 10 minutes + eventApiTimeout: 1000, + segmentsApiTimeout: 1000, + } +}); +``` + +### v6 (After) + +```javascript +const odpManager = createOdpManager({ + segmentsCacheSize: 100, + segmentsCacheTimeout: 600000, // 10 minutes + eventApiTimeout: 1000, + segmentsApiTimeout: 1000, + eventBatchSize: 5, // Now configurable in browser + eventFlushInterval: 3000, // Now configurable in browser +}); + +const optimizely = createInstance({ + projectConfigManager, + odpManager +}); +``` + +To disable ODP functionality in v6, simply don't provide an ODP Manager to the client instance. + +## VUID Management + +In v6, VUID tracking is disabled by default and must be explicitly enabled by createing a vuidManager with `enableVuid` set to `true` and passing it to `createInstance`: + +```javascript +const vuidManager = createVuidManager({ + enableVuid: true, // Explicitly enable VUID tracking +}); + +const optimizely = createInstance({ + projectConfigManager, + vuidManager +}); +``` + +## Error Handling + +Error handling in v6 uses a new errorNotifier object: + +### v5 (Before) + +```javascript +const optimizely = createInstance({ + errorHandler: { + handleError: (error) => { + console.error("Custom error handler", error); + } + } +}); +``` + +### v6 (After) + +```javascript +const errorNotifier = createErrorNotifier({ + handleError: (error) => { + console.error("Custom error handler", error); + } +}); + +const optimizely = createInstance({ + projectConfigManager, + errorNotifier +}); +``` + +## Logging + +Logging in v6 is disabled by defualt, and must be enabled by passing in a logger created via a factory function: + +### v5 (Before) + +```javascript +const optimizely = createInstance({ + logLevel: LogLevel.DEBUG +}); +``` + +### v6 (After) + +```javascript +import { createLogger, DEBUG } from "@optimizely/optimizely-sdk"; + +const logger = createLogger({ + level: DEBUG +}); + +const optimizely = createInstance({ + projectConfigManager, + logger +}); +``` + +## onReady Promise Behavior + +The `onReady()` method behavior has changed in v6. In v5, onReady() fulfilled with an object that had two fields: `success` and `reason`. If the instance failed to initialize, `success` would be `false` and `reason` will contain an error message. In v6, if onReady() fulfills, that means the instance is ready to use, the fulfillment value is of unknown type and need not to be inspected. If the promise rejects, that means there was an error during initialization. + +### v5 (Before) + +```javascript +optimizely.onReady().then(({ success, reason }) => { + if (success) { + // optimizely is ready to use + } else { + console.log(`initialization unsuccessful: ${reason}`); + } +}); +``` + +### v6 (After) + +```javascript +optimizely + .onReady() + .then(() => { + // optimizely is ready to use + console.log("Client is ready"); + }) + .catch((err) => { + console.error("Error initializing Optimizely client:", err); + }); +``` + +## Migration Examples + +### Basic Example with SDK Key + +#### v5 (Before) + +```javascript +import { createInstance } from '@optimizely/optimizely-sdk'; + +const optimizely = createInstance({ + sdkKey: '<YOUR_SDK_KEY>' +}); + +optimizely.onReady().then(({ success }) => { + if (success) { + // Use the client + } +}); +``` + +#### v6 (After) + +```javascript +import { + createInstance, + createPollingProjectConfigManager +} from '@optimizely/optimizely-sdk'; + +const projectConfigManager = createPollingProjectConfigManager({ + sdkKey: '<YOUR_SDK_KEY>' +}); + +const optimizely = createInstance({ + projectConfigManager +}); + +optimizely + .onReady() + .then(() => { + // Use the client + }) + .catch(err => { + console.error(err); + }); +``` + +### Complete Example with ODP and Event Batching + +#### v5 (Before) + +```javascript +import { createInstance, LogLevel } from '@optimizely/optimizely-sdk'; + +const optimizely = createInstance({ + sdkKey: '<YOUR_SDK_KEY>', + datafileOptions: { + autoUpdate: true, + updateInterval: 60000 // 1 minute + }, + eventBatchSize: 3, + eventFlushInterval: 10000, // 10 seconds + logLevel: LogLevel.DEBUG, + odpOptions: { + segmentsCacheSize: 10, + segmentsCacheTimeout: 60000 // 1 minute + } +}); + +optimizely.notificationCenter.addNotificationListener( + enums.NOTIFICATION_TYPES.TRACK, + (payload) => { + console.log("Track event", payload); + } +); +``` + +#### v6 (After) + +```javascript +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + createOdpManager, + createLogger, + DEBUG, + NOTIFICATION_TYPES +} from '@optimizely/optimizely-sdk'; + +const projectConfigManager = createPollingProjectConfigManager({ + sdkKey: '<YOUR_SDK_KEY>', + autoUpdate: true, + updateInterval: 60000 // 1 minute +}); + +const batchEventProcessor = createBatchEventProcessor({ + batchSize: 3, + flushInterval: 10000, // 10 seconds +}); + +const odpManager = createOdpManager({ + segmentsCacheSize: 10, + segmentsCacheTimeout: 60000 // 1 minute +}); + +const logger = createLogger({ + level: DEBUG +}); + +const optimizely = createInstance({ + projectConfigManager, + eventProcessor: batchEventProcessor, + odpManager, + logger +}); + +optimizely.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + (payload) => { + console.log("Track event", payload); + } +); +``` + +For complete implementation examples, refer to the [Optimizely JavaScript SDK documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-browser-sdk-v6). diff --git a/README.md b/README.md index 64b80f460..08ab9f5ad 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,280 @@ -<h3 align="center"> - Optimizely JavaScript SDK -</h3> +# Optimizely JavaScript SDK -<p align="center"> - This repository houses the official JavaScript SDK for use with Optimizely X Full Stack. -</p> +[![npm](https://img.shields.io/npm/v/%40optimizely%2Foptimizely-sdk.svg)](https://www.npmjs.com/package/@optimizely/optimizely-sdk) +[![npm](https://img.shields.io/npm/dm/%40optimizely%2Foptimizely-sdk.svg)](https://www.npmjs.com/package/@optimizely/optimizely-sdk) +[![GitHub Actions](https://img.shields.io/github/actions/workflow/status/optimizely/javascript-sdk/javascript.yml)](https://github.com/optimizely/javascript-sdk/actions) +[![Coveralls](https://img.shields.io/coveralls/optimizely/javascript-sdk.svg)](https://coveralls.io/github/optimizely/javascript-sdk) +[![license](https://img.shields.io/github/license/optimizely/javascript-sdk.svg)](https://choosealicense.com/licenses/apache-2.0/) -Optimizely X Full Stack is A/B testing and feature management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at https://www.optimizely.com/products/full-stack/, or see the [documentation](https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=node). +This is the official JavaScript and TypeScript SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). The SDK now features a modular architecture for greater flexibility and control. If you're upgrading from a previous version, see our [Migration Guide](MIGRATION.md). -## Packages +Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/introduction). -This repository is a monorepo that we manage using [Lerna](https://github.com/lerna/lerna). Only one package lives here currently, but that may change in the future. +Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap. -| Package | Version | Docs | Description | -| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| [`@optimizely/optimizely-sdk`](/packages/optimizely-sdk) | [![npm](https://img.shields.io/npm/v/%40optimizely%2Foptimizely-sdk.svg)](https://www.npmjs.com/package/@optimizely/optimizely-sdk) | [![](https://img.shields.io/badge/API%20Docs-site-green.svg?style=flat-square)](https://developers.optimizely.com/x/solutions/sdks/reference/?language=javascript) | The Optimizely SDK | +--- -## About +## Get Started -`@optimizely/optimizely-sdk` is developed and maintained by [Optimizely](https://optimizely.com) and many [contributors](https://github.com/optimizely/javascript-sdk/graphs/contributors). If you're interested in learning more about what Optimizely X Full Stack can do for your company, please [get in touch](mailto:eng@optimizely.com)! +> Refer to the [JavaScript SDK's developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk) for detailed instructions on getting started with using the SDK. +> For **Edge Functions**, we provide starter kits that utilize the Optimizely JavaScript SDK for the following platforms: +> +> - [Akamai (Edgeworkers)](https://github.com/optimizely/akamai-edgeworker-starter-kit) +> - [AWS Lambda@Edge](https://github.com/optimizely/aws-lambda-at-edge-starter-kit) +> - [Cloudflare Worker](https://github.com/optimizely/cloudflare-worker-template) +> - [Fastly Compute@Edge](https://github.com/optimizely/fastly-compute-starter-kit) +> - [Vercel Edge Middleware](https://github.com/optimizely/vercel-examples/tree/main/edge-middleware/feature-flag-optimizely) +> +> Note: We recommend using the **Lite** entrypoint (for version < 6) / **Universal** entrypoint (for version >=6) of the sdk for edge platforms. These starter kits also use the **Lite** variant of the JavaScript SDK. + +### Prerequisites + +Ensure the SDK supports all of the platforms you're targeting. In particular, the SDK targets modern ES6-compliant JavaScript environments. We officially support: + +- Node.js >= 18.0.0. By extension, environments like AWS Lambda, Google Cloud Functions, and Auth0 Webtasks are supported as well. Older Node.js releases likely work too (try `npm test` to validate for yourself), but are not formally supported. +- Modern Web Browsers, such as Microsoft Edge 84+, Firefox 91+, Safari 13+, and Chrome 102+, Opera 76+ + +In addition, other environments are likely compatible but are not formally supported including: + +- Progressive Web Apps, WebViews, and hybrid mobile apps like those built with React Native and Apache Cordova. +- [Cloudflare Workers](https://developers.cloudflare.com/workers/) and [Fly](https://fly.io/), both of which are powered by recent releases of V8. +- Anywhere else you can think of that might embed a JavaScript engine. The sky is the limit; experiment everywhere! 🚀 + +### Install the SDK + +Once you've validated that the SDK supports the platforms you're targeting, fetch the package from [NPM](https://www.npmjs.com/package/@optimizely/optimizely-sdk): + +Using `npm`: + +```sh +npm install --save @optimizely/optimizely-sdk +``` + +Using `yarn`: + +```sh +yarn add @optimizely/optimizely-sdk +``` + +Using `pnpm`: + +```sh +pnpm add @optimizely/optimizely-sdk +``` + +Using `deno` (no installation required): + +```javascript +import optimizely from 'npm:@optimizely/optimizely-sdk'; +``` + +## Use the JavaScript SDK + +See the [JavaScript SDK's developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-sdk) to learn how to set up your first JavaScript project using the SDK. + +The SDK uses a modular architecture with dedicated components for project configuration, event processing, and more. The examples below demonstrate the recommended initialization pattern. + +### Initialization with Package Managers (npm, yarn, pnpm) + +```javascript +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + createOdpManager, +} from '@optimizely/optimizely-sdk'; + +// 1. Configure your project config manager +const pollingConfigManager = createPollingProjectConfigManager({ + sdkKey: '<YOUR_SDK_KEY>', + autoUpdate: true, // Optional: enable automatic updates + updateInterval: 300000, // Optional: update every 5 minutes (in ms) +}); + +// 2. Create an event processor for analytics +const batchEventProcessor = createBatchEventProcessor({ + batchSize: 10, // Optional: default batch size + flushInterval: 1000, // Optional: flush interval in ms +}); + +// 3. Set up ODP manager for segments and audience targeting +const odpManager = createOdpManager(); + +// 4. Initialize the Optimizely client with the components +const optimizelyClient = createInstance({ + projectConfigManager: pollingConfigManager, + eventProcessor: batchEventProcessor, + odpManager: odpManager, +}); + +optimizelyClient + .onReady() + .then(() => { + console.log('Optimizely client is ready'); + // Your application code using Optimizely goes here + }) + .catch(error => { + console.error('Error initializing Optimizely client:', error); + }); +``` + +### Initialization (Using HTML script tag) + +The package has different entry points for different environments. The browser entry point is an ES module, which can be used with an appropriate bundler like **Webpack** or **Rollup**. Additionally, for ease of use during initial evaluations you can include a standalone umd bundle of the SDK in your web page by fetching it from [unpkg](https://unpkg.com/): + +```html +<script src="https://unpkg.com/@optimizely/optimizely-sdk@6/dist/optimizely.browser.umd.min.js"></script> + +<!-- You can also use the unminified version if necessary --> +<script src="https://unpkg.com/@optimizely/optimizely-sdk@6/dist/optimizely.browser.umd.js"></script> +``` + +> ⚠️ **Warning**: Always include a specific version number (such as @6) when using CDN URLs like the `unpkg` example above. If you use a URL without a version, your application may automatically receive breaking changes when a new major version is released, which can lead to unexpected issues. + +When evaluated, that bundle assigns the SDK's exports to `window.optimizelySdk`. If you wish to use the asset locally (for example, if unpkg is down), you can find it in your local copy of the package at dist/optimizely.browser.umd.min.js. We do not recommend using this method in production settings as it introduces a third-party performance dependency. + +As `window.optimizelySdk` should be a global variable at this point, you can continue to use it like so: + +```html +<script> + // Extract the factory functions from the global SDK + const { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + createOdpManager, + } = window.optimizelySdk; + + // Initialize components + const pollingConfigManager = createPollingProjectConfigManager({ + sdkKey: '<YOUR_SDK_KEY>', + autoUpdate: true, + }); + + const batchEventProcessor = createBatchEventProcessor(); + + const odpManager = createOdpManager(); + + // Create the Optimizely client + const optimizelyClient = createInstance({ + projectConfigManager: pollingConfigManager, + eventProcessor: batchEventProcessor, + odpManager: odpManager, + }); + + optimizelyClient + .onReady() + .then(() => { + console.log('Optimizely client is ready'); + // Start using the client here + }) + .catch(error => { + console.error('Error initializing Optimizely client:', error); + }); +</script> +``` + +### Closing the SDK Instance + +Depending on the sdk configuration, the client instance might schedule tasks in the background. If the instance has background tasks scheduled, +then the instance will not be garbage collected even though there are no more references to the instance in the code. (Basically, the background tasks will still hold references to the instance). Therefore, it's important to close it to properly clean up resources. + +```javascript +// Close the Optimizely client when you're done using it +optimizelyClient.close() +``` +Using the following settings will cause background tasks to be scheduled + +- Polling Datafile Manager +- Batch Event Processor with batchSize > 1 +- ODP manager with eventBatchSize > 1 + + + +> ⚠️ **Warning**: Failure to close SDK instances when they're no longer needed may result in memory leaks. This is particularly important for applications that create multiple instances over time. For some environment like SSR applications, it might not be convenient to close each instance, in which case, the `disposable` option of `createInstance` can be used to disable all background tasks on the server side, allowing the instance to be garbage collected. + + +## Special Notes + +### Migration Guides + +If you're updating your SDK version, please check the appropriate migration guide: + +- **Migrating from 5.x or lower to 6.x**: See our [Migration Guide](MIGRATION.md) for detailed instructions on updating to the new modular architecture. +- **Migrating from 4.x or lower to 5.x**: Please refer to the [Changelog](CHANGELOG.md#500---january-19-2024) for details on these breaking changes. + +## SDK Development + +### Unit Tests + +There is a mix of testing paradigms used within the JavaScript SDK which include Mocha, Chai, Karma, and Vitest, indicated by their respective `*.tests.js` and `*.spec.ts` filenames. + +When contributing code to the SDK, aim to keep the percentage of code test coverage at the current level ([![Coveralls](https://img.shields.io/coveralls/optimizely/javascript-sdk.svg)](https://coveralls.io/github/optimizely/javascript-sdk)) or above. + +To run unit tests, you can take the following steps: + +1. Ensure that you have run `npm install` to install all project dependencies. +2. Run `npm test` to run all test files. +3. Run `npm run test-vitest` to run only tests written using Vitest. +4. Run `npm run test-mocha` to run only tests written using Mocha. +4. (For cross-browser testing) Run `npm run test-xbrowser` to run tests in many browsers via BrowserStack. +5. Resolve any tests that fail before continuing with your contribution. + +This information is relevant only if you plan on contributing to the SDK itself. + +```sh +# Prerequisite: Install dependencies. +npm install + +# Run unit tests. +npm test + +# Run unit tests in many browsers, currently via BrowserStack. +# For this to work, the following environment variables must be set: +# - BROWSER_STACK_USERNAME +# - BROWSER_STACK_PASSWORD +npm run test-xbrowser +``` + +[/.github/workflows/javascript.yml](/.github/workflows/javascript.yml) contains the definitions for `BROWSER_STACK_USERNAME` and `BROWSER_STACK_ACCESS_KEY` used in the GitHub Actions CI pipeline. When developing locally, you must provide your own credentials in order to run `npm run test-xbrowser`. You can register for an account for free on [the BrowserStack official website here](https://www.browserstack.com/). + ### Contributing -Please see [CONTRIBUTING](CONTRIBUTING.md). +For more information regarding contributing to the Optimizely JavaScript SDK, please read [Contributing](CONTRIBUTING.md). + + +### Feature Management access + +To access the Feature Management configuration in the Optimizely dashboard, please contact your Optimizely customer success manager. + +## Credits + +`@optimizely/optimizely-sdk` is developed and maintained by [Optimizely](https://optimizely.com) and many [contributors](https://github.com/optimizely/javascript-sdk/graphs/contributors). If you're interested in learning more about what Optimizely Feature Experimentation can do for your company you can visit the [official Optimizely Feature Experimentation product page here](https://www.optimizely.com/products/experiment/feature-experimentation/) to learn more. + +First-party code (under `lib/`) is copyright Optimizely, Inc., licensed under Apache 2.0. + +### Other Optimizely SDKs + +- Agent - https://github.com/optimizely/agent + +- Android - https://github.com/optimizely/android-sdk + +- C# - https://github.com/optimizely/csharp-sdk + +- Flutter - https://github.com/optimizely/optimizely-flutter-sdk + +- Go - https://github.com/optimizely/go-sdk + +- Java - https://github.com/optimizely/java-sdk + +- PHP - https://github.com/optimizely/php-sdk + +- Python - https://github.com/optimizely/python-sdk + +- React - https://github.com/optimizely/react-sdk + +- Ruby - https://github.com/optimizely/ruby-sdk + +- Swift - https://github.com/optimizely/swift-sdk diff --git a/__mocks__/@react-native-async-storage/async-storage-event-processor.ts b/__mocks__/@react-native-async-storage/async-storage-event-processor.ts new file mode 100644 index 000000000..ad40f0152 --- /dev/null +++ b/__mocks__/@react-native-async-storage/async-storage-event-processor.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +let items: {[key: string]: string} = {} + +export default class AsyncStorage { + + static getItem(key: string, callback?: (error?: Error, result?: string) => void): Promise<string | null> { + return new Promise(resolve => { + setTimeout(() => resolve(items[key] || null), 1) + }) + } + + static setItem(key: string, value: string, callback?: (error?: Error) => void): Promise<void> { + return new Promise((resolve) => { + setTimeout(() => { + items[key] = value + resolve() + }, 1) + }) + } + + static removeItem(key: string, callback?: (error?: Error, result?: string) => void): Promise<string | null> { + return new Promise(resolve => { + setTimeout(() => { + items[key] && delete items[key] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + resolve() + }, 1) + }) + } + + static dumpItems(): {[key: string]: string} { + return items + } + + static clearStore(): void { + items = {} + } +} diff --git a/__mocks__/@react-native-async-storage/async-storage.ts b/__mocks__/@react-native-async-storage/async-storage.ts new file mode 100644 index 000000000..36d3cf85d --- /dev/null +++ b/__mocks__/@react-native-async-storage/async-storage.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class AsyncStorage { + private static items: Record<string, string> = {}; + + static getItem( + key: string, + callback?: (error?: Error, result?: string | null) => void + ): Promise<string | null> { + const value = AsyncStorage.items[key] || null; + callback?.(undefined, value); + return Promise.resolve(value); + } + + static setItem( + key: string, + value: string, + callback?: (error?: Error) => void + ): Promise<void> { + AsyncStorage.items[key] = value; + callback?.(undefined); + return Promise.resolve(); + } + + static removeItem( + key: string, + callback?: (error?: Error, result?: string | null) => void + ): Promise<string | null> { + const value = AsyncStorage.items[key] || null; + if (key in AsyncStorage.items) { + delete AsyncStorage.items[key]; + } + callback?.(undefined, value); + return Promise.resolve(value); + } + + static clearStore(): Promise<void> { + AsyncStorage.items = {}; + return Promise.resolve(); + } + +} diff --git a/packages/optimizely-sdk/lib/utils/string_value_validator/index.js b/__mocks__/@react-native-community/netinfo.ts similarity index 65% rename from packages/optimizely-sdk/lib/utils/string_value_validator/index.js rename to __mocks__/@react-native-community/netinfo.ts index f968a50ab..12fab972a 100644 --- a/packages/optimizely-sdk/lib/utils/string_value_validator/index.js +++ b/__mocks__/@react-native-community/netinfo.ts @@ -1,5 +1,5 @@ /** - * Copyright 2018, Optimizely + * Copyright 2022, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +let localCallback: any -module.exports = { - /** - * Validates provided value is a non-empty string - * @param {string} input - * @return {boolean} True for non-empty string, false otherwise - */ - validate: function(input) { - return typeof input === 'string' && input !== ''; - } -}; +export function addEventListener(callback: any) { + localCallback = callback +} + +export function triggerInternetState(isInternetReachable: boolean) { + localCallback({ isInternetReachable }) +} diff --git a/__mocks__/fast-text-encoding.ts b/__mocks__/fast-text-encoding.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/__mocks__/fast-text-encoding.ts @@ -0,0 +1 @@ +export {}; diff --git a/__mocks__/react-native-get-random-values.ts b/__mocks__/react-native-get-random-values.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/__mocks__/react-native-get-random-values.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/optimizely-sdk/karma.base.conf.js b/karma.base.conf.js similarity index 71% rename from packages/optimizely-sdk/karma.base.conf.js rename to karma.base.conf.js index 7fdff6cf6..9691083ef 100644 --- a/packages/optimizely-sdk/karma.base.conf.js +++ b/karma.base.conf.js @@ -1,5 +1,5 @@ /** - * Copyright 2018, Optimizely + * Copyright 2018, 2020, 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,12 +24,24 @@ module.exports = { webpack: { mode: 'production', + module: { + rules: [ + { + test: /\.[tj]s$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, }, //browserStack setup browserStack: { username: process.env.BROWSER_STACK_USERNAME, - accessKey: process.env.BROWSER_STACK_ACCESS_KEY + accessKey: process.env.BROWSER_STACK_ACCESS_KEY, }, // to avoid DISCONNECTED messages when connecting to BrowserStack @@ -42,89 +54,75 @@ module.exports = { customLaunchers: { bs_chrome_mac: { base: 'BrowserStack', - browser: 'chrome', - browser_version: '21.0', os: 'OS X', - os_version: 'Mountain Lion' + os_version: 'Mojave', + browserName: 'Chrome', + browser_version: '102.0', + browser: 'Chrome', }, bs_edge: { base: 'BrowserStack', os: 'Windows', os_version: '10', - browser: 'edge', - device: null, - browser_version: '15.0' + browserName: 'Edge', + browser_version: '84.0', + browser: 'Edge', }, - bs_firefox_mac: { - base: 'BrowserStack', - browser: 'firefox', - browser_version: '21.0', - os: 'OS X', - os_version: 'Mountain Lion' - }, - bs_ie: { + bs_firefox: { base: 'BrowserStack', os: 'Windows', - os_version: '7', - browser: 'ie', - device: null, - browser_version: '10.0' - }, - bs_iphone6: { - base: 'BrowserStack', - device: 'iPhone 6', - os: 'ios', - os_version: '8.3' + browser: 'Firefox', + os_version: '10', + browserName: 'Firefox', + browser_version: '91.0', }, + bs_opera_mac: { base: 'BrowserStack', - browser: 'opera', - browser_version: '37', + browser: 'Opera', + os_version: 'Mojave', + browserName: 'Opera', + browser: 'Opera', + browser_version: '76.0', os: 'OS X', - os_version: 'Mountain Lion' }, bs_safari: { base: 'BrowserStack', os: 'OS X', - os_version: 'Mountain Lion', - browser: 'safari', + os_version: 'Catalina', + browserName: 'Safari', + browser_version: '13.0', + browser: 'Safari', device: null, - browser_version: '6.2' - } + }, }, - browsers: ['bs_chrome_mac', 'bs_edge', 'bs_firefox_mac', 'bs_ie', 'bs_iphone6', 'bs_opera_mac', 'bs_safari'], + browsers: ['bs_chrome_mac', 'bs_edge', 'bs_firefox', 'bs_opera_mac', 'bs_safari'], // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha'], // list of files to exclude - exclude: [ - ], - + exclude: [], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - './lib/**/*tests.js': ['webpack'] + './lib/**/*tests.js': ['webpack'], }, - // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['progress'], - // web server port port: 9876, - // enable / disable colors in the output (reporters and logs) colors: true, - // enable / disable watching file and executing tests whenever any file changes autoWatch: false, @@ -134,5 +132,5 @@ module.exports = { // Concurrency level // how many browser should be started simultaneous - concurrency: Infinity -} + concurrency: Infinity, +}; diff --git a/packages/optimizely-sdk/karma.bs.conf.js b/karma.bs.conf.js similarity index 89% rename from packages/optimizely-sdk/karma.bs.conf.js rename to karma.bs.conf.js index d7b8df6ce..7b7ce8cbb 100644 --- a/packages/optimizely-sdk/karma.bs.conf.js +++ b/karma.bs.conf.js @@ -15,7 +15,7 @@ */ // Karma configuration for cross-browser testing -const baseConfig = require('./karma.base.conf.js') +const baseConfig = require('./karma.base.conf.js'); module.exports = function(config) { config.set({ @@ -27,6 +27,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ + './node_modules/promise-polyfill/dist/polyfill.min.js', './lib/index.browser.tests.js' ], }); diff --git a/packages/optimizely-sdk/lib/plugins/error_handler/index.tests.js b/karma.local_chrome.bs.conf.js similarity index 59% rename from packages/optimizely-sdk/lib/plugins/error_handler/index.tests.js rename to karma.local_chrome.bs.conf.js index 753ec01a1..7b6fc0b8a 100644 --- a/packages/optimizely-sdk/lib/plugins/error_handler/index.tests.js +++ b/karma.local_chrome.bs.conf.js @@ -1,5 +1,5 @@ /** - * Copyright 2016, Optimizely + * Copyright 2021 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var errorHandler = require('./'); +const baseConfig = require('./karma.base.conf'); -var chai = require('chai'); -var assert = chai.assert; - -describe('lib/plugins/error_handler', function() { - describe('APIs', function() { - describe('handleError', function() { - it('should just be a no-op function', function() { - assert.isFunction(errorHandler.handleError); - }); - }); +module.exports = function(config) { + config.set({ + ...baseConfig, + plugins: ['karma-mocha', 'karma-webpack', 'karma-chrome-launcher'], + browserStack: null, + browsers: ['Chrome'], + files: [ + './node_modules/promise-polyfill/dist/polyfill.min.js', + './lib/index.browser.tests.js' + ], }); -}); +} diff --git a/karma.local_chrome.umd.conf.js b/karma.local_chrome.umd.conf.js new file mode 100644 index 000000000..9c5da74c2 --- /dev/null +++ b/karma.local_chrome.umd.conf.js @@ -0,0 +1,30 @@ +/** + * Copyright 2021 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const baseConfig = require('./karma.base.conf'); + +module.exports = function(config) { + config.set({ + ...baseConfig, + plugins: ['karma-mocha', 'karma-webpack', 'karma-chrome-launcher'], + browserStack: null, + browsers: ['Chrome'], + files: [ + './node_modules/promise-polyfill/dist/polyfill.min.js', + './dist/optimizely.browser.umd.min.js', + './lib/index.browser.umdtests.js' + ], + }); +} diff --git a/packages/optimizely-sdk/karma.umd.conf.js b/karma.umd.conf.js similarity index 90% rename from packages/optimizely-sdk/karma.umd.conf.js rename to karma.umd.conf.js index ea0f59e27..51d621238 100644 --- a/packages/optimizely-sdk/karma.umd.conf.js +++ b/karma.umd.conf.js @@ -15,7 +15,7 @@ */ // Karma configuration for UMD bundle testing -const baseConfig = require('./karma.base.conf.js') +const baseConfig = require('./karma.base.conf.js'); module.exports = function(config) { config.set({ @@ -27,6 +27,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ + './node_modules/promise-polyfill/dist/polyfill.min.js', './dist/optimizely.browser.umd.min.js', './lib/index.browser.umdtests.js' ], diff --git a/lerna.json b/lerna.json deleted file mode 100644 index b9c6fb446..000000000 --- a/lerna.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "lerna": "3.2.1", - "version": "independent", - "packages": [ - "packages/*" - ] -} diff --git a/lib/client_factory.spec.ts b/lib/client_factory.spec.ts new file mode 100644 index 000000000..1aa09bda0 --- /dev/null +++ b/lib/client_factory.spec.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; + +import { getOptimizelyInstance } from './client_factory'; +import { createStaticProjectConfigManager } from './project_config/config_manager_factory'; +import Optimizely from './optimizely'; + +describe('getOptimizelyInstance', () => { + it('should throw if the projectConfigManager is not a valid ProjectConfigManager', () => { + expect(() => getOptimizelyInstance({ + projectConfigManager: undefined as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: null as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: 'abc' as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: 123 as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + + expect(() => getOptimizelyInstance({ + projectConfigManager: {} as any, + requestHandler: {} as any, + })).toThrow('Invalid config manager'); + }); + + it('should return an instance of Optimizely if a valid projectConfigManager is provided', () => { + const optimizelyInstance = getOptimizelyInstance({ + projectConfigManager: createStaticProjectConfigManager({ + datafile: '{}', + }), + requestHandler: {} as any, + }); + + expect(optimizelyInstance).toBeInstanceOf(Optimizely); + }); +}); diff --git a/lib/client_factory.ts b/lib/client_factory.ts new file mode 100644 index 000000000..87a239246 --- /dev/null +++ b/lib/client_factory.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Client, Config } from "./shared_types"; +import { extractLogger } from "./logging/logger_factory"; +import { extractErrorNotifier } from "./error/error_notifier_factory"; +import { extractConfigManager } from "./project_config/config_manager_factory"; +import { extractEventProcessor } from "./event_processor/event_processor_factory"; +import { extractOdpManager } from "./odp/odp_manager_factory"; +import { extractVuidManager } from "./vuid/vuid_manager_factory"; +import { RequestHandler } from "./utils/http_request_handler/http"; +import { CLIENT_VERSION, DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT, JAVASCRIPT_CLIENT_ENGINE } from "./utils/enums"; +import Optimizely from "./optimizely"; +import { DefaultCmabClient } from "./core/decision_service/cmab/cmab_client"; +import { CmabCacheValue, DefaultCmabService } from "./core/decision_service/cmab/cmab_service"; +import { InMemoryLruCache } from "./utils/cache/in_memory_lru_cache"; + +export type OptimizelyFactoryConfig = Config & { + requestHandler: RequestHandler; +} + +export const getOptimizelyInstance = (config: OptimizelyFactoryConfig): Optimizely => { + const { + clientEngine, + clientVersion, + jsonSchemaValidator, + userProfileService, + userProfileServiceAsync, + defaultDecideOptions, + disposable, + requestHandler, + } = config; + + const projectConfigManager = extractConfigManager(config.projectConfigManager); + const eventProcessor = extractEventProcessor(config.eventProcessor); + const odpManager = extractOdpManager(config.odpManager); + const vuidManager = extractVuidManager(config.vuidManager); + const errorNotifier = extractErrorNotifier(config.errorNotifier); + const logger = extractLogger(config.logger); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const cmabService = new DefaultCmabService({ + cmabClient, + cmabCache: new InMemoryLruCache<CmabCacheValue>(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT), + }); + + const optimizelyOptions = { + cmabService, + clientEngine: clientEngine || JAVASCRIPT_CLIENT_ENGINE, + clientVersion: clientVersion || CLIENT_VERSION, + jsonSchemaValidator, + userProfileService, + userProfileServiceAsync, + defaultDecideOptions, + disposable, + logger, + errorNotifier, + projectConfigManager, + eventProcessor, + odpManager, + vuidManager, + }; + + return new Optimizely(optimizelyOptions); +} diff --git a/lib/common_exports.ts b/lib/common_exports.ts new file mode 100644 index 000000000..801fb7728 --- /dev/null +++ b/lib/common_exports.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createStaticProjectConfigManager } from './project_config/config_manager_factory'; + +export { LogLevel } from './logging/logger'; + +export { + DEBUG, + INFO, + WARN, + ERROR, +} from './logging/logger_factory'; + +export { createLogger } from './logging/logger_factory'; +export { createErrorNotifier } from './error/error_notifier_factory'; + +export { + DECISION_SOURCES, +} from './utils/enums'; + +export { NOTIFICATION_TYPES, DECISION_NOTIFICATION_TYPES } from './notification_center/type'; + +export { OptimizelyDecideOption } from './shared_types'; diff --git a/lib/core/audience_evaluator/index.spec.ts b/lib/core/audience_evaluator/index.spec.ts new file mode 100644 index 000000000..e22654144 --- /dev/null +++ b/lib/core/audience_evaluator/index.spec.ts @@ -0,0 +1,713 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { beforeEach, afterEach, describe, it, vi, expect, afterAll } from 'vitest'; + +import AudienceEvaluator, { createAudienceEvaluator } from './index'; +import * as conditionTreeEvaluator from '../condition_tree_evaluator'; +import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; +import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE } from '../../message/log_message'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { Audience, OptimizelyDecideOption, OptimizelyDecision } from '../../shared_types'; +import { IOptimizelyUserContext } from '../../optimizely_user_context'; + +let mockLogger = getMockLogger(); + +const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimizelyUserContext => ({ + getAttributes: () => ({ ...(attributes || {}) }), + isQualifiedFor: segment => segments ? segments.indexOf(segment) > -1 : false, + qualifiedSegments: segments || [], + getUserId: () => 'mockUserId', + setAttribute: (key: string, value: any) => {}, + + decide: (key: string, options?: OptimizelyDecideOption[]): OptimizelyDecision => ({ + variationKey: 'mockVariationKey', + enabled: true, + variables: { mockVariable: 'mockValue' }, + ruleKey: 'mockRuleKey', + reasons: ['mockReason'], + flagKey: 'flagKey', + userContext: getMockUserContext() + }), +}) as IOptimizelyUserContext; + +const chromeUserAudience = { + id: '0', + name: 'chromeUserAudience', + conditions: [ + 'and', + { + name: 'browser_type', + value: 'chrome', + type: 'custom_attribute', + }, + ], +}; +const iphoneUserAudience = { + id: '1', + name: 'iphoneUserAudience', + conditions: [ + 'and', + { + name: 'device_model', + value: 'iphone', + type: 'custom_attribute', + }, + ], +}; +const specialConditionTypeAudience = { + id: '3', + name: 'specialConditionTypeAudience', + conditions: [ + 'and', + { + match: 'interest_level', + value: 'special', + type: 'special_condition_type', + }, + ], +}; +const conditionsPassingWithNoAttrs = [ + 'not', + { + match: 'exists', + name: 'input_value', + type: 'custom_attribute', + }, +]; +const conditionsPassingWithNoAttrsAudience = { + id: '2', + name: 'conditionsPassingWithNoAttrsAudience', + conditions: conditionsPassingWithNoAttrs, +}; + +const audiencesById: { +[id: string]: Audience; +} = { + "0": chromeUserAudience, + "1": iphoneUserAudience, + "2": conditionsPassingWithNoAttrsAudience, + "3": specialConditionTypeAudience, +}; + + +describe('lib/core/audience_evaluator', () => { + let audienceEvaluator: AudienceEvaluator; + + beforeEach(() => { + mockLogger = getMockLogger(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('APIs', () => { + describe('with default condition evaluator', () => { + beforeEach(() => { + audienceEvaluator = createAudienceEvaluator({}); + }); + describe('evaluate', () => { + it('should return true if there are no audiences', () => { + expect(audienceEvaluator.evaluate([], audiencesById, getMockUserContext({}))).toBe(true); + }); + + it('should return false if there are audiences but no attributes', () => { + expect(audienceEvaluator.evaluate(['0'], audiencesById, getMockUserContext({}))).toBe(false); + }); + + it('should return true if any of the audience conditions are met', () => { + const iphoneUsers = { + device_model: 'iphone', + }; + + const chromeUsers = { + browser_type: 'chrome', + }; + + const iphoneChromeUsers = { + browser_type: 'chrome', + device_model: 'iphone', + }; + + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneUsers))).toBe(true); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(chromeUsers))).toBe(true); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneChromeUsers))).toBe( + true + ); + }); + + it('should return false if none of the audience conditions are met', () => { + const nexusUsers = { + device_model: 'nexus5', + }; + + const safariUsers = { + browser_type: 'safari', + }; + + const nexusSafariUsers = { + browser_type: 'safari', + device_model: 'nexus5', + }; + + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(nexusUsers))).toBe(false); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(safariUsers))).toBe(false); + expect(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(nexusSafariUsers))).toBe( + false + ); + }); + + it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', () => { + expect(audienceEvaluator.evaluate(['2'], audiencesById, getMockUserContext({}))).toBe(true); + }); + + describe('complex audience conditions', () => { + it('should return true if any of the audiences in an "OR" condition pass', () => { + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }) + ); + expect(result).toBe(true); + }); + + it('should return true if all of the audiences in an "AND" condition pass', () => { + const result = audienceEvaluator.evaluate( + ['and', '0', '1'], + audiencesById, + getMockUserContext({ + browser_type: 'chrome', + device_model: 'iphone', + }) + ); + expect(result).toBe(true); + }); + + it('should return true if the audience in a "NOT" condition does not pass', () => { + const result = audienceEvaluator.evaluate( + ['not', '1'], + audiencesById, + getMockUserContext({ device_model: 'android' }) + ); + expect(result).toBe(true); + }); + }); + + describe('integration with dependencies', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + afterAll(() => { + vi.resetAllMocks(); + }); + + it('returns true if conditionTreeEvaluator.evaluate returns true', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(true); + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }) + ); + expect(result).toBe(true); + }); + + it('returns false if conditionTreeEvaluator.evaluate returns false', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(false); + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ browser_type: 'safari' }) + ); + expect(result).toBe(false); + }); + + it('returns false if conditionTreeEvaluator.evaluate returns null', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockReturnValue(null); + const result = audienceEvaluator.evaluate( + ['or', '0', '1'], + audiencesById, + getMockUserContext({ state: 'California' }) + ); + expect(result).toBe(false); + }); + + it('calls customAttributeConditionEvaluator.evaluate in the leaf evaluator for audience conditions', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementation((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + const mockCustomAttributeConditionEvaluator = vi.fn().mockReturnValue(false); + + vi.spyOn(customAttributeConditionEvaluator, 'getEvaluator').mockReturnValue({ + evaluate: mockCustomAttributeConditionEvaluator, + }); + + const audienceEvaluator = createAudienceEvaluator({}); + + const userAttributes = { device_model: 'android' }; + const user = getMockUserContext(userAttributes); + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith( + iphoneUserAudience.conditions[1], + user, + ); + + expect(result).toBe(false); + }); + }); + + describe('Audience evaluation logging', () => { + let mockCustomAttributeConditionEvaluator: ReturnType<typeof vi.fn>; + + beforeEach(() => { + mockCustomAttributeConditionEvaluator = vi.fn(); + vi.spyOn(conditionTreeEvaluator, 'evaluate'); + vi.spyOn(customAttributeConditionEvaluator, 'getEvaluator').mockReturnValue({ + evaluate: mockCustomAttributeConditionEvaluator, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns null', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.mockReturnValue(null); + const userAttributes = { device_model: 5.5 }; + const user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); + expect(result).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledTimes(2); + + expect(mockLogger.debug).toHaveBeenCalledWith( + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ); + + expect(mockLogger.debug).toHaveBeenCalledWith(AUDIENCE_EVALUATION_RESULT, '1', 'UNKNOWN'); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns true', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.mockReturnValue(true); + + const userAttributes = { device_model: 'iphone' }; + const user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); + expect(result).toBe(true); + expect(mockLogger.debug).toHaveBeenCalledTimes(2) + expect(mockLogger.debug).toHaveBeenCalledWith( + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ); + + expect(mockLogger.debug).toHaveBeenCalledWith(AUDIENCE_EVALUATION_RESULT, '1', 'TRUE'); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns false', () => { + vi.spyOn(conditionTreeEvaluator, 'evaluate').mockImplementationOnce((conditions: any, leafEvaluator) => { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.mockReturnValue(false); + + const userAttributes = { device_model: 'android' }; + const user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + const result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledTimes(1); + expect(mockCustomAttributeConditionEvaluator).toHaveBeenCalledWith(iphoneUserAudience.conditions[1], user); + expect(result).toBe(false); + expect(mockLogger.debug).toHaveBeenCalledTimes(2) + expect(mockLogger.debug).toHaveBeenCalledWith( + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ); + + expect(mockLogger.debug).toHaveBeenCalledWith(AUDIENCE_EVALUATION_RESULT, '1', 'FALSE'); + }); + }); + }); + }); + + describe('with additional custom condition evaluator', () => { + describe('when passing a valid additional evaluator', () => { + beforeEach(() => { + const mockEnvironment = { + special: true, + }; + audienceEvaluator = createAudienceEvaluator({ + special_condition_type: { + evaluate: (condition: any, user: any) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = mockEnvironment[condition.value] && user.getAttributes()[condition.match] > 0; + return result; + }, + }, + }); + }); + + it('should evaluate an audience properly using the custom condition evaluator', () => { + expect(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 0 }))).toBe( + false + ); + expect(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 1 }))).toBe( + true + ); + }); + }); + + describe('when passing an invalid additional evaluator', () => { + beforeEach(() => { + audienceEvaluator = createAudienceEvaluator({ + custom_attribute: { + evaluate: () => { + return false; + }, + }, + }); + }); + + it('should not be able to overwrite built in `custom_attribute` evaluator', () => { + expect( + audienceEvaluator.evaluate( + ['0'], + audiencesById, + getMockUserContext({ + browser_type: 'chrome', + }) + ) + ).toBe(true); + }); + }); + }); + + describe('with odp segment evaluator', () => { + describe('Single ODP Audience', () => { + const singleAudience = { + id: '0', + name: 'singleAudience', + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + const audiencesById = { + 0: singleAudience, + }; + const audience = new AudienceEvaluator({}); + + it('should evaluate to true if segment is found', () => { + expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1']))).toBe(true); + }); + + it('should evaluate to false if segment is not found', () => { + expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-2']))).toBe(false); + }); + + it('should evaluate to false if not segments are provided', () => { + expect(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}))).toBe(false); + }); + }); + + describe('Multiple ODP conditions in one Audience', () => { + const singleAudience = { + id: '0', + name: 'singleAudience', + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + [ + 'or', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-4', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + }; + const audiencesById = { + 0: singleAudience, + }; + const audience = new AudienceEvaluator({}); + + it('should evaluate correctly based on the given segments', () => { + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(false); + expect( + audience.evaluate( + ['or', '0'], + audiencesById, + getMockUserContext({}, ['odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(false); + }); + }); + + describe('Multiple ODP conditions in multiple Audience', () => { + const audience1And2 = { + id: '0', + name: 'audience1And2', + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + + const audience3And4 = { + id: '1', + name: 'audience3And4', + conditions: [ + 'and', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-4', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + + const audience5And6 = { + id: '2', + name: 'audience5And6', + conditions: [ + 'or', + { + value: 'odp-segment-5', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-6', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + const audiencesById = { + 0: audience1And2, + 1: audience3And4, + 2: audience5And6, + }; + const audience = new AudienceEvaluator({}); + + it('should evaluate correctly based on the given segments', () => { + expect( + audience.evaluate( + ['or', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2']) + ) + ).toBe(true); + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2']) + ) + ).toBe(false); + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, [ + 'odp-segment-1', + 'odp-segment-2', + 'odp-segment-3', + 'odp-segment-4', + 'odp-segment-6', + ]) + ) + ).toBe(true); + expect( + audience.evaluate( + ['and', '0', '1', ['not', '2']], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ).toBe(true); + }); + }); + }); + + describe('with multiple types of evaluators', () => { + const audience1And2 = { + id: '0', + name: 'audience1And2', + conditions: [ + 'and', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + const audience3Or4 = { + id: '', + name: 'audience3And4', + conditions: [ + 'or', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'odp-segment-4', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + }; + + const audiencesById = { + 0: audience1And2, + 1: audience3Or4, + 2: chromeUserAudience, + }; + + const audience = new AudienceEvaluator({}); + + it('should evaluate correctly based on the given segments', () => { + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'not_chrome' }, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ).toBe(false); + expect( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ).toBe(true); + }); + }); + }); +}); diff --git a/lib/core/audience_evaluator/index.tests.js b/lib/core/audience_evaluator/index.tests.js new file mode 100644 index 000000000..1dc5efd30 --- /dev/null +++ b/lib/core/audience_evaluator/index.tests.js @@ -0,0 +1,644 @@ +/** + * Copyright 2016, 2018-2020, 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sinon from 'sinon'; +import { assert } from 'chai'; +import { sprintf } from '../../utils/fns'; + +import AudienceEvaluator, { createAudienceEvaluator } from './index'; +import * as conditionTreeEvaluator from '../condition_tree_evaluator'; +import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; +import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE } from 'log_message'; + +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); +var mockLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} + +var getMockUserContext = (attributes, segments) => ({ + getAttributes: () => ({ ... (attributes || {})}), + isQualifiedFor: segment => segments.indexOf(segment) > -1 +}); + +var chromeUserAudience = { + conditions: [ + 'and', + { + name: 'browser_type', + value: 'chrome', + type: 'custom_attribute', + }, + ], +}; +var iphoneUserAudience = { + conditions: [ + 'and', + { + name: 'device_model', + value: 'iphone', + type: 'custom_attribute', + }, + ], +}; +var specialConditionTypeAudience = { + conditions: [ + 'and', + { + match: 'interest_level', + value: 'special', + type: 'special_condition_type', + }, + ], +}; +var conditionsPassingWithNoAttrs = [ + 'not', + { + match: 'exists', + name: 'input_value', + type: 'custom_attribute', + }, +]; +var conditionsPassingWithNoAttrsAudience = { + conditions: conditionsPassingWithNoAttrs, +}; +var audiencesById = { + 0: chromeUserAudience, + 1: iphoneUserAudience, + 2: conditionsPassingWithNoAttrsAudience, + 3: specialConditionTypeAudience, +}; + +describe('lib/core/audience_evaluator', function() { + var audienceEvaluator; + + beforeEach(function() { + sinon.stub(mockLogger, 'info'); + sinon.stub(mockLogger, 'debug'); + sinon.stub(mockLogger, 'warn'); + sinon.stub(mockLogger, 'error'); + }); + + afterEach(function() { + mockLogger.info.restore(); + mockLogger.debug.restore(); + mockLogger.warn.restore(); + mockLogger.error.restore(); + }); + + describe('APIs', function() { + context('with default condition evaluator', function() { + beforeEach(function() { + audienceEvaluator = createAudienceEvaluator(); + }); + describe('evaluate', function() { + it('should return true if there are no audiences', function() { + assert.isTrue(audienceEvaluator.evaluate([], audiencesById, getMockUserContext({}))); + }); + + it('should return false if there are audiences but no attributes', function() { + assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, getMockUserContext({}))); + }); + + it('should return true if any of the audience conditions are met', function() { + var iphoneUsers = { + device_model: 'iphone', + }; + + var chromeUsers = { + browser_type: 'chrome', + }; + + var iphoneChromeUsers = { + browser_type: 'chrome', + device_model: 'iphone', + }; + + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneUsers))); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(chromeUsers))); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneChromeUsers))); + }); + + it('should return false if none of the audience conditions are met', function() { + var nexusUsers = { + device_model: 'nexus5', + }; + + var safariUsers = { + browser_type: 'safari', + }; + + var nexusSafariUsers = { + browser_type: 'safari', + device_model: 'nexus5', + }; + + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(nexusUsers))); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(safariUsers))); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(nexusSafariUsers))); + }); + + it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', function() { + assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, getMockUserContext({}))); + }); + + describe('complex audience conditions', function() { + it('should return true if any of the audiences in an "OR" condition pass', function() { + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'chrome' })); + assert.isTrue(result); + }); + + it('should return true if all of the audiences in an "AND" condition pass', function() { + var result = audienceEvaluator.evaluate(['and', '0', '1'], audiencesById, getMockUserContext({ + browser_type: 'chrome', + device_model: 'iphone', + })); + assert.isTrue(result); + }); + + it('should return true if the audience in a "NOT" condition does not pass', function() { + var result = audienceEvaluator.evaluate(['not', '1'], audiencesById, getMockUserContext({ device_model: 'android' })); + assert.isTrue(result); + }); + }); + + describe('integration with dependencies', function() { + var sandbox = sinon.sandbox.create(); + + beforeEach(function() { + sandbox.stub(conditionTreeEvaluator, 'evaluate'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('returns true if conditionTreeEvaluator.evaluate returns true', function() { + conditionTreeEvaluator.evaluate.returns(true); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'chrome' })); + assert.isTrue(result); + }); + + it('returns false if conditionTreeEvaluator.evaluate returns false', function() { + conditionTreeEvaluator.evaluate.returns(false); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'safari' })); + assert.isFalse(result); + }); + + it('returns false if conditionTreeEvaluator.evaluate returns null', function() { + conditionTreeEvaluator.evaluate.returns(null); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ state: 'California' })); + assert.isFalse(result); + }); + + it('calls customAttributeConditionEvaluator.evaluate in the leaf evaluator for audience conditions', function() { + conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { + return leafEvaluator(conditions[1]); + }); + + const mockCustomAttributeConditionEvaluator = sinon.stub().returns(false); + + sinon.stub(customAttributeConditionEvaluator, 'getEvaluator').returns({ + evaluate: mockCustomAttributeConditionEvaluator, + }); + + const audienceEvaluator = createAudienceEvaluator(); + + var userAttributes = { device_model: 'android' }; + var user = getMockUserContext(userAttributes); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + sinon.assert.calledOnce(mockCustomAttributeConditionEvaluator); + sinon.assert.calledWithExactly( + mockCustomAttributeConditionEvaluator, + iphoneUserAudience.conditions[1], + user, + ); + assert.isFalse(result); + + customAttributeConditionEvaluator.getEvaluator.restore(); + }); + }); + + describe('Audience evaluation logging', function() { + var sandbox = sinon.sandbox.create(); + var mockCustomAttributeConditionEvaluator; + + beforeEach(function() { + mockCustomAttributeConditionEvaluator = sinon.stub(); + sandbox.stub(conditionTreeEvaluator, 'evaluate'); + sandbox.stub(customAttributeConditionEvaluator, 'getEvaluator').returns({ + evaluate: mockCustomAttributeConditionEvaluator, + }); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns null', function() { + conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.returns(null); + var userAttributes = { device_model: 5.5 }; + var user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + + sinon.assert.calledOnce(mockCustomAttributeConditionEvaluator); + sinon.assert.calledWithExactly( + mockCustomAttributeConditionEvaluator, + iphoneUserAudience.conditions[1], + user + ); + assert.isFalse(result); + assert.strictEqual(2, mockLogger.debug.callCount); + + sinon.assert.calledWithExactly( + mockLogger.debug, + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ) + + sinon.assert.calledWithExactly( + mockLogger.debug, + AUDIENCE_EVALUATION_RESULT, + '1', + 'UNKNOWN' + ) + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns true', function() { + conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.returns(true); + + var userAttributes = { device_model: 'iphone' }; + var user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + sinon.assert.calledOnce(mockCustomAttributeConditionEvaluator); + sinon.assert.calledWithExactly( + mockCustomAttributeConditionEvaluator, + iphoneUserAudience.conditions[1], + user, + ); + assert.isTrue(result); + assert.strictEqual(2, mockLogger.debug.callCount); + sinon.assert.calledWithExactly( + mockLogger.debug, + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ) + + sinon.assert.calledWithExactly( + mockLogger.debug, + AUDIENCE_EVALUATION_RESULT, + '1', + 'TRUE' + ) + }); + + it('logs correctly when conditionTreeEvaluator.evaluate returns false', function() { + conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { + return leafEvaluator(conditions[1]); + }); + + mockCustomAttributeConditionEvaluator.returns(false); + + var userAttributes = { device_model: 'android' }; + var user = getMockUserContext(userAttributes); + + const audienceEvaluator = createAudienceEvaluator({}, mockLogger); + + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); + sinon.assert.calledOnce(mockCustomAttributeConditionEvaluator); + sinon.assert.calledWithExactly( + mockCustomAttributeConditionEvaluator, + iphoneUserAudience.conditions[1], + user, + ); + assert.isFalse(result); + assert.strictEqual(2, mockLogger.debug.callCount); + + sinon.assert.calledWithExactly( + mockLogger.debug, + EVALUATING_AUDIENCE, + '1', + JSON.stringify(['and', iphoneUserAudience.conditions[1]]) + ) + + sinon.assert.calledWithExactly( + mockLogger.debug, + AUDIENCE_EVALUATION_RESULT, + '1', + 'FALSE' + ) + }); + }); + }); + }); + + context('with additional custom condition evaluator', function() { + describe('when passing a valid additional evaluator', function() { + beforeEach(function() { + const mockEnvironment = { + special: true, + }; + audienceEvaluator = createAudienceEvaluator({ + special_condition_type: { + evaluate: function(condition, user) { + const result = mockEnvironment[condition.value] && user.getAttributes()[condition.match] > 0; + return result; + }, + }, + }); + }); + + it('should evaluate an audience properly using the custom condition evaluator', function() { + assert.isFalse(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 0 }))); + assert.isTrue(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 1 }))); + }); + }); + + describe('when passing an invalid additional evaluator', function() { + beforeEach(function() { + audienceEvaluator = createAudienceEvaluator({ + custom_attribute: { + evaluate: function() { + return false; + }, + }, + }); + }); + + it('should not be able to overwrite built in `custom_attribute` evaluator', function() { + assert.isTrue( + audienceEvaluator.evaluate(['0'], audiencesById, getMockUserContext({ + browser_type: 'chrome', + })) + ); + }); + }); + }); + + context('with odp segment evaluator', function() { + describe('Single ODP Audience', () => { + const singleAudience = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + const audiencesById = { + 0: singleAudience, + } + const audience = new AudienceEvaluator(); + + it('should evaluate to true if segment is found', () => { + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1']))); + }); + + it('should evaluate to false if segment is not found', () => { + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-2']))); + }); + + it('should evaluate to false if not segments are provided', () => { + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}))); + }); + }); + + describe('Multiple ODP conditions in one Audience', () => { + const singleAudience = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-4", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + ] + ] + }; + const audiencesById = { + 0: singleAudience, + } + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']))); + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']))); + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']))); + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-3', 'odp-segment-4']))); + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-2', 'odp-segment-3', 'odp-segment-4']))); + }); + }); + + describe('Multiple ODP conditions in multiple Audience', () => { + const audience1And2 = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + + const audience3And4 = { + "conditions": [ + "and", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-4", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + + const audience5And6 = { + "conditions": [ + "or", + { + "value": "odp-segment-5", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-6", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + const audiencesById = { + 0: audience1And2, + 1: audience3And4, + 2: audience5And6 + } + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + assert.isTrue( + audience.evaluate( + ['or', '0', '1', '2'], + audiencesById, + getMockUserContext({},['odp-segment-1', 'odp-segment-2']) + ) + ); + assert.isFalse( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2']) + ) + ); + assert.isTrue( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4', 'odp-segment-6']) + ) + ); + assert.isTrue( + audience.evaluate( + ['and', '0', '1',['not', '2']], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ); + }); + }); + }); + + context('with multiple types of evaluators', function() { + const audience1And2 = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + const audience3Or4 = { + "conditions": [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-4", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + + const audiencesById = { + 0: audience1And2, + 1: audience3Or4, + 2: chromeUserAudience, + } + + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + assert.isFalse( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'not_chrome' }, + ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ); + assert.isTrue( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }, + ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ); + }); + }); + }); +}); diff --git a/lib/core/audience_evaluator/index.ts b/lib/core/audience_evaluator/index.ts new file mode 100644 index 000000000..e2b3bce0a --- /dev/null +++ b/lib/core/audience_evaluator/index.ts @@ -0,0 +1,121 @@ +/** + * Copyright 2016, 2018-2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as conditionTreeEvaluator from '../condition_tree_evaluator'; +import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; +import * as odpSegmentsConditionEvaluator from './odp_segment_condition_evaluator'; +import { Audience, Condition, OptimizelyUserContext } from '../../shared_types'; +import { CONDITION_EVALUATOR_ERROR, UNKNOWN_CONDITION_TYPE } from 'error_message'; +import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE} from 'log_message'; +import { LoggerFacade } from '../../logging/logger'; + +export class AudienceEvaluator { + private logger?: LoggerFacade; + + private typeToEvaluatorMap: { + [key: string]: { + [key: string]: (condition: Condition, user: OptimizelyUserContext) => boolean | null + }; + }; + + /** + * Construct an instance of AudienceEvaluator with given options + * @param {Object=} UNSTABLE_conditionEvaluators A map of condition evaluators provided by the consumer. This enables matching + * condition types which are not supported natively by the SDK. Note that built in + * Optimizely evaluators cannot be overridden. + * @constructor + */ + constructor(UNSTABLE_conditionEvaluators: unknown, logger?: LoggerFacade) { + this.logger = logger; + this.typeToEvaluatorMap = { + ...UNSTABLE_conditionEvaluators as any, + custom_attribute: customAttributeConditionEvaluator.getEvaluator(this.logger), + third_party_dimension: odpSegmentsConditionEvaluator.getEvaluator(this.logger), + }; + } + + /** + * Determine if the given user attributes satisfy the given audience conditions + * @param {Array<string|string[]} audienceConditions Audience conditions to match the user attributes against - can be an array + * of audience IDs, a nested array of conditions, or a single leaf condition. + * Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"], "1" + * @param {[id: string]: Audience} audiencesById Object providing access to full audience objects for audience IDs + * contained in audienceConditions. Keys should be audience IDs, values + * should be full audience objects with conditions properties + * @param {OptimizelyUserContext} userAttributes User context which contains the attributes and segments which will be used in + * determining if audience conditions are met. + * @return {boolean} true if the user attributes match the given audience conditions, false + * otherwise + */ + evaluate( + audienceConditions: Array<string | string[]>, + audiencesById: { [id: string]: Audience }, + user: OptimizelyUserContext, + ): boolean { + // if there are no audiences, return true because that means ALL users are included in the experiment + if (!audienceConditions || audienceConditions.length === 0) { + return true; + } + + const evaluateAudience = (audienceId: string) => { + const audience = audiencesById[audienceId]; + if (audience) { + this.logger?.debug( + EVALUATING_AUDIENCE, audienceId, JSON.stringify(audience.conditions) + ); + const result = conditionTreeEvaluator.evaluate( + audience.conditions as unknown[] , + this.evaluateConditionWithUserAttributes.bind(this, user) + ); + const resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase(); + this.logger?.debug(AUDIENCE_EVALUATION_RESULT, audienceId, resultText); + return result; + } + return null; + }; + + return !!conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience); + } + + /** + * Wrapper around evaluator.evaluate that is passed to the conditionTreeEvaluator. + * Evaluates the condition provided given the user attributes if an evaluator has been defined for the condition type. + * @param {OptimizelyUserContext} user Optimizely user context containing attributes and segments + * @param {Condition} condition A single condition object to evaluate. + * @return {boolean|null} true if the condition is satisfied, null if a matcher is not found. + */ + evaluateConditionWithUserAttributes(user: OptimizelyUserContext, condition: Condition): boolean | null { + const evaluator = this.typeToEvaluatorMap[condition.type]; + if (!evaluator) { + this.logger?.warn(UNKNOWN_CONDITION_TYPE, JSON.stringify(condition)); + return null; + } + try { + return evaluator.evaluate(condition, user); + } catch (err: any) { + this.logger?.error( + CONDITION_EVALUATOR_ERROR, condition.type, err.message + ); + } + + return null; + } +} + +export default AudienceEvaluator; + +export const createAudienceEvaluator = function(UNSTABLE_conditionEvaluators: unknown, logger?: LoggerFacade): AudienceEvaluator { + return new AudienceEvaluator(UNSTABLE_conditionEvaluators, logger); +}; diff --git a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts new file mode 100644 index 000000000..f42d07cb4 --- /dev/null +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.spec.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { afterEach, describe, it, vi, expect } from 'vitest'; +import * as odpSegmentEvalutor from '.'; +import { UNKNOWN_MATCH_TYPE } from '../../../message/error_message'; +import { IOptimizelyUserContext } from '../../../optimizely_user_context'; +import { OptimizelyDecideOption, OptimizelyDecision } from '../../../shared_types'; +import { getMockLogger } from '../../../tests/mock/mock_logger'; + +const odpSegment1Condition = { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" +}; + +const getMockUserContext = (attributes?: unknown, segments?: string[]): IOptimizelyUserContext => ({ + getAttributes: () => ({ ...(attributes || {}) }), + isQualifiedFor: segment => segments ? segments.indexOf(segment) > -1 : false, + qualifiedSegments: segments || [], + getUserId: () => 'mockUserId', + setAttribute: (key: string, value: any) => {}, + + decide: (key: string, options?: OptimizelyDecideOption[]): OptimizelyDecision => ({ + variationKey: 'mockVariationKey', + enabled: true, + variables: { mockVariable: 'mockValue' }, + ruleKey: 'mockRuleKey', + reasons: ['mockReason'], + flagKey: 'flagKey', + userContext: getMockUserContext() + }), +}) as IOptimizelyUserContext; + + +describe('lib/core/audience_evaluator/odp_segment_condition_evaluator', function() { + const mockLogger = getMockLogger(); + const { evaluate } = odpSegmentEvalutor.getEvaluator(mockLogger); + + afterEach(function() { + vi.restoreAllMocks(); + }); + + it('should return true when segment qualifies and known match type is provided', () => { + expect(evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-1']))).toBe(true); + }); + + it('should return false when segment does not qualify and known match type is provided', () => { + expect(evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-2']))).toBe(false); + }) + + it('should return null when segment qualifies but unknown match type is provided', () => { + const invalidOdpMatchCondition = { + ... odpSegment1Condition, + "match": 'unknown', + }; + expect(evaluate(invalidOdpMatchCondition, getMockUserContext({}, ['odp-segment-1']))).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(UNKNOWN_MATCH_TYPE, JSON.stringify(invalidOdpMatchCondition)); + }); +}); diff --git a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.tests.js b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.tests.js new file mode 100644 index 000000000..cc9218887 --- /dev/null +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.tests.js @@ -0,0 +1,76 @@ +/**************************************************************************** + * Copyright 2022, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import sinon from 'sinon'; +import { assert } from 'chai'; +import { sprintf } from '../../../utils/fns'; + +import { LOG_LEVEL } from '../../../utils/enums'; +import * as odpSegmentEvalutor from './'; +import { UNKNOWN_MATCH_TYPE } from 'error_message'; + +var odpSegment1Condition = { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" +}; + +var getMockUserContext = (attributes, segments) => ({ + getAttributes: () => ({ ... (attributes || {})}), + isQualifiedFor: segment => segments.indexOf(segment) > -1 +}); + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +describe('lib/core/audience_evaluator/odp_segment_condition_evaluator', function() { + const mockLogger = createLogger(); + const { evaluate } = odpSegmentEvalutor.getEvaluator(mockLogger); + + beforeEach(function() { + sinon.stub(mockLogger, 'warn'); + sinon.stub(mockLogger, 'error'); + }); + + afterEach(function() { + mockLogger.warn.restore(); + mockLogger.error.restore(); + }); + + it('should return true when segment qualifies and known match type is provided', () => { + assert.isTrue(evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-1']))); + }); + + it('should return false when segment does not qualify and known match type is provided', () => { + assert.isFalse(evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-2']))); + }) + + it('should return null when segment qualifies but unknown match type is provided', () => { + const invalidOdpMatchCondition = { + ... odpSegment1Condition, + "match": 'unknown', + }; + assert.isNull(evaluate(invalidOdpMatchCondition, getMockUserContext({}, ['odp-segment-1']))); + sinon.assert.calledOnce(mockLogger.warn); + assert.strictEqual(mockLogger.warn.args[0][0], UNKNOWN_MATCH_TYPE); + assert.strictEqual(mockLogger.warn.args[0][1], JSON.stringify(invalidOdpMatchCondition)); + }); +}); diff --git a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts new file mode 100644 index 000000000..7380c9269 --- /dev/null +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts @@ -0,0 +1,68 @@ +/**************************************************************************** + * Copyright 2022 Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import { UNKNOWN_MATCH_TYPE } from 'error_message'; +import { LoggerFacade } from '../../../logging/logger'; +import { Condition, OptimizelyUserContext } from '../../../shared_types'; + +const QUALIFIED_MATCH_TYPE = 'qualified'; + +const MATCH_TYPES = [ + QUALIFIED_MATCH_TYPE, +]; + +type ConditionEvaluator = (condition: Condition, user: OptimizelyUserContext) => boolean | null; +type Evaluator = { evaluate: (condition: Condition, user: OptimizelyUserContext) => boolean | null; } + +const EVALUATORS_BY_MATCH_TYPE: { [conditionType: string]: ConditionEvaluator | undefined } = {}; +EVALUATORS_BY_MATCH_TYPE[QUALIFIED_MATCH_TYPE] = qualifiedEvaluator; + +export const getEvaluator = (logger?: LoggerFacade): Evaluator => { + return { + evaluate(condition: Condition, user: OptimizelyUserContext): boolean | null { + return evaluate(condition, user, logger); + } + }; +} + +/** + * Given a custom attribute audience condition and user attributes, evaluate the + * condition against the attributes. + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @return {?boolean} true/false if the given user attributes match/don't match the given condition, + * null if the given user attributes and condition can't be evaluated + * TODO: Change to accept and object with named properties + */ +function evaluate(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const conditionMatch = condition.match; + if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { + logger?.warn(UNKNOWN_MATCH_TYPE, JSON.stringify(condition)); + return null; + } + + let evaluator; + if (!conditionMatch) { + evaluator = qualifiedEvaluator; + } else { + evaluator = EVALUATORS_BY_MATCH_TYPE[conditionMatch] || qualifiedEvaluator; + } + + return evaluator(condition, user); +} + +function qualifiedEvaluator(condition: Condition, user: OptimizelyUserContext): boolean { + return user.isQualifiedFor(condition.value as string); +} diff --git a/lib/core/bucketer/bucket_value_generator.spec.ts b/lib/core/bucketer/bucket_value_generator.spec.ts new file mode 100644 index 000000000..e68db6348 --- /dev/null +++ b/lib/core/bucketer/bucket_value_generator.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, describe, it } from 'vitest'; +import { sprintf } from '../../utils/fns'; +import { generateBucketValue } from './bucket_value_generator'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { INVALID_BUCKETING_ID } from 'error_message'; + +describe('generateBucketValue', () => { + it('should return a bucket value for different inputs', () => { + const experimentId = 1886780721; + const bucketingKey1 = sprintf('%s%s', 'ppid1', experimentId); + const bucketingKey2 = sprintf('%s%s', 'ppid2', experimentId); + const bucketingKey3 = sprintf('%s%s', 'ppid2', 1886780722); + const bucketingKey4 = sprintf('%s%s', 'ppid3', experimentId); + + expect(generateBucketValue(bucketingKey1)).toBe(5254); + expect(generateBucketValue(bucketingKey2)).toBe(4299); + expect(generateBucketValue(bucketingKey3)).toBe(2434); + expect(generateBucketValue(bucketingKey4)).toBe(5439); + }); + + it('should return an error if it cannot generate the hash value', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => generateBucketValue(null)).toThrow(OptimizelyError); + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + generateBucketValue(null); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_BUCKETING_ID); + } + }); +}); diff --git a/lib/core/bucketer/bucket_value_generator.ts b/lib/core/bucketer/bucket_value_generator.ts new file mode 100644 index 000000000..c5f85303b --- /dev/null +++ b/lib/core/bucketer/bucket_value_generator.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import murmurhash from 'murmurhash'; +import { INVALID_BUCKETING_ID } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +const HASH_SEED = 1; +const MAX_HASH_VALUE = Math.pow(2, 32); +const MAX_TRAFFIC_VALUE = 10000; + +/** + * Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE) + * @param {string} bucketingKey String value for bucketing + * @return {number} The generated bucket value + * @throws If bucketing value is not a valid string + */ +export const generateBucketValue = function(bucketingKey: string): number { + try { + // NOTE: the mmh library already does cast the hash value as an unsigned 32bit int + // https://github.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115 + const hashValue = murmurhash.v3(bucketingKey, HASH_SEED); + const ratio = hashValue / MAX_HASH_VALUE; + return Math.floor(ratio * MAX_TRAFFIC_VALUE); + } catch (ex) { + throw new OptimizelyError(INVALID_BUCKETING_ID, bucketingKey, ex.message); + } +}; diff --git a/lib/core/bucketer/index.spec.ts b/lib/core/bucketer/index.spec.ts new file mode 100644 index 000000000..942295356 --- /dev/null +++ b/lib/core/bucketer/index.spec.ts @@ -0,0 +1,402 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { sprintf } from '../../utils/fns'; +import projectConfig, { ProjectConfig } from '../../project_config/project_config'; +import { getTestProjectConfig } from '../../tests/test_data'; +import { INVALID_BUCKETING_ID, INVALID_GROUP_ID } from 'error_message'; +import * as bucketer from './'; +import * as bucketValueGenerator from './bucket_value_generator'; + +import { + USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + USER_NOT_IN_ANY_EXPERIMENT, + USER_ASSIGNED_TO_EXPERIMENT_BUCKET, +} from '.'; +import { BucketerParams } from '../../shared_types'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { LoggerFacade } from '../../logging/logger'; + +const testData = getTestProjectConfig(); + +function cloneDeep<T>(value: T): T { + if (value === null || typeof value !== 'object') { + return value; + } + + if (Array.isArray(value)) { + return (value.map(cloneDeep) as unknown) as T; + } + + const copy: Record<string, unknown> = {}; + + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + copy[key] = cloneDeep((value as Record<string, unknown>)[key]); + } + } + + return copy as T; +} + +const setLogSpy = (logger: LoggerFacade) => { + vi.spyOn(logger, 'info'); + vi.spyOn(logger, 'debug'); + vi.spyOn(logger, 'warn'); + vi.spyOn(logger, 'error'); +}; + +describe('excluding groups', () => { + let configObj; + const mockLogger = getMockLogger(); + let bucketerParams: BucketerParams; + + beforeEach(() => { + setLogSpy(mockLogger); + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: configObj.experiments[0].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: mockLogger, + validateEntity: true, + }; + + vi.spyOn(bucketValueGenerator, 'generateBucketValue') + .mockReturnValueOnce(50) + .mockReturnValueOnce(50000); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return decision response with correct variation ID when provided bucket value', async () => { + const bucketerParamsTest1 = cloneDeep(bucketerParams); + bucketerParamsTest1.userId = 'ppid1'; + const decisionResponse = bucketer.bucket(bucketerParamsTest1); + + expect(decisionResponse.result).toBe('111128'); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'ppid1'); + + const bucketerParamsTest2 = cloneDeep(bucketerParams); + bucketerParamsTest2.userId = 'ppid2'; + const decisionResponse2 = bucketer.bucket(bucketerParamsTest2); + + expect(decisionResponse2.result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'ppid2'); + }); +}); + +describe('including groups: random', () => { + let configObj: ProjectConfig; + const mockLogger = getMockLogger(); + let bucketerParams: BucketerParams; + + beforeEach(() => { + setLogSpy(mockLogger); + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bucketerParams = { + experimentId: configObj.experiments[4].id, + experimentKey: configObj.experiments[4].key, + trafficAllocationConfig: configObj.experiments[4].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: mockLogger, + userId: 'testUser', + validateEntity: true, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return decision response with the proper variation for a user in a grouped experiment', () => { + vi.spyOn(bucketValueGenerator, 'generateBucketValue') + .mockReturnValueOnce(50) + .mockReturnValueOnce(50); + + const decisionResponse = bucketer.bucket(bucketerParams); + + expect(decisionResponse.result).toBe('551'); + expect(mockLogger.info).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledTimes(2); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'testUser'); + expect(mockLogger.info).toHaveBeenCalledWith( + USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + 'testUser', + 'groupExperiment1', + '666' + ); + }); + + it('should return decision response with variation null when a user is bucketed into a different grouped experiment than the one speicfied', () => { + vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValue(5000); + + const decisionResponse = bucketer.bucket(bucketerParams); + + expect(decisionResponse.result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'testUser'); + expect(mockLogger.info).toHaveBeenCalledWith( + USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + 'testUser', + 'groupExperiment1', + '666' + ); + }); + + it('should return decision response with variation null when a user is not bucketed into any experiments in the random group', () => { + vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValue(50000); + + const decisionResponse = bucketer.bucket(bucketerParams); + + expect(decisionResponse.result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'testUser'); + expect(mockLogger.info).toHaveBeenCalledWith(USER_NOT_IN_ANY_EXPERIMENT, 'testUser', '666'); + }); + + it('should return decision response with variation null when a user is bucketed into traffic space of deleted experiment within a random group', () => { + vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValueOnce(9000); + + const decisionResponse = bucketer.bucket(bucketerParams); + + expect(decisionResponse.result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'testUser'); + expect(mockLogger.info).toHaveBeenCalledWith(USER_NOT_IN_ANY_EXPERIMENT, 'testUser', '666'); + }); + + it('should throw an error if group ID is not in the datafile', () => { + const bucketerParamsWithInvalidGroupId = cloneDeep(bucketerParams); + bucketerParamsWithInvalidGroupId.experimentIdMap[configObj.experiments[4].id].groupId = '6969'; + + expect(()=> bucketer.bucket(bucketerParamsWithInvalidGroupId)).toThrow(OptimizelyError); + + try { + bucketer.bucket(bucketerParamsWithInvalidGroupId); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_GROUP_ID); + } + }); +}); + +describe('including groups: overlapping', () => { + let configObj: ProjectConfig; + const mockLogger = getMockLogger(); + let bucketerParams: BucketerParams; + + beforeEach(() => { + setLogSpy(mockLogger); + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bucketerParams = { + experimentId: configObj.experiments[6].id, + experimentKey: configObj.experiments[6].key, + trafficAllocationConfig: configObj.experiments[6].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: mockLogger, + userId: 'testUser', + validateEntity: true, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return decision response with variation when a user falls into an experiment within an overlapping group', () => { + vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValueOnce(0); + + const decisionResponse = bucketer.bucket(bucketerParams); + + expect(decisionResponse.result).toBe('553'); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(USER_ASSIGNED_TO_EXPERIMENT_BUCKET, expect.any(Number), 'testUser'); + }); + + it('should return decision response with variation null when a user does not fall into an experiment within an overlapping group', () => { + vi.spyOn(bucketValueGenerator, 'generateBucketValue').mockReturnValueOnce(3000); + const decisionResponse = bucketer.bucket(bucketerParams); + + expect(decisionResponse.result).toBeNull(); + }); +}); + +describe('bucket value falls into empty traffic allocation ranges', () => { + let configObj: ProjectConfig; + const mockLogger = getMockLogger(); + let bucketerParams: BucketerParams; + + beforeEach(() => { + setLogSpy(mockLogger); + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: [ + { + entityId: '', + endOfRange: 5000, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: mockLogger, + validateEntity: true, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return decision response with variation null', () => { + const bucketerParamsTest1 = cloneDeep(bucketerParams); + bucketerParamsTest1.userId = 'ppid1'; + const decisionResponse = bucketer.bucket(bucketerParamsTest1); + + expect(decisionResponse.result).toBeNull(); + }); + + it('should not log an invalid variation ID warning', () => { + bucketer.bucket(bucketerParams); + + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); +}); + +describe('traffic allocation has invalid variation ids', () => { + let configObj: ProjectConfig; + const mockLogger = getMockLogger(); + let bucketerParams: BucketerParams; + + beforeEach(() => { + setLogSpy(mockLogger); + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: [ + { + entityId: '-1', + endOfRange: 5000, + }, + { + entityId: '-2', + endOfRange: 10000, + }, + ], + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: mockLogger, + validateEntity: true, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return decision response with variation null', () => { + const bucketerParamsTest1 = cloneDeep(bucketerParams); + bucketerParamsTest1.userId = 'ppid1'; + const decisionResponse = bucketer.bucket(bucketerParamsTest1); + + expect(decisionResponse.result).toBeNull(); + }); +}); + + + +describe('testBucketWithBucketingId', () => { + let bucketerParams: BucketerParams; + + beforeEach(() => { + const configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bucketerParams = { + trafficAllocationConfig: configObj.experiments[0].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + validateEntity: true, + }; + }); + + it('check that a non null bucketingId buckets a variation different than the one expected with userId', () => { + const bucketerParams1 = cloneDeep(bucketerParams); + bucketerParams1['userId'] = 'testBucketingIdControl'; + bucketerParams1['bucketingId'] = '123456789'; + bucketerParams1['experimentKey'] = 'testExperiment'; + bucketerParams1['experimentId'] = '111127'; + + expect(bucketer.bucket(bucketerParams1).result).toBe('111129'); + }); + + it('check that a null bucketing ID defaults to bucketing with the userId', () => { + const bucketerParams2 = cloneDeep(bucketerParams); + bucketerParams2['userId'] = 'testBucketingIdControl'; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bucketerParams2['bucketingId'] = null; + bucketerParams2['experimentKey'] = 'testExperiment'; + bucketerParams2['experimentId'] = '111127'; + + expect(bucketer.bucket(bucketerParams2).result).toBe('111128'); + }); + + it('check that bucketing works with an experiment in group', () => { + const bucketerParams4 = cloneDeep(bucketerParams); + bucketerParams4['userId'] = 'testBucketingIdControl'; + bucketerParams4['bucketingId'] = '123456789'; + bucketerParams4['experimentKey'] = 'groupExperiment2'; + bucketerParams4['experimentId'] = '443'; + + expect(bucketer.bucket(bucketerParams4).result).toBe('111128'); + }); +}); diff --git a/lib/core/bucketer/index.tests.js b/lib/core/bucketer/index.tests.js new file mode 100644 index 000000000..a1e046088 --- /dev/null +++ b/lib/core/bucketer/index.tests.js @@ -0,0 +1,406 @@ +/** + * Copyright 2016-2017, 2019-2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sinon from 'sinon'; +import { assert, expect } from 'chai'; +import { cloneDeep, create } from 'lodash'; +import { sprintf } from '../../utils/fns'; +import * as bucketValueGenerator from './bucket_value_generator' +import * as bucketer from './'; +import { LOG_LEVEL } from '../../utils/enums'; +import projectConfig from '../../project_config/project_config'; +import { getTestProjectConfig } from '../../tests/test_data'; +import { INVALID_BUCKETING_ID, INVALID_GROUP_ID } from 'error_message'; +import { + USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + USER_NOT_IN_ANY_EXPERIMENT, + USER_ASSIGNED_TO_EXPERIMENT_BUCKET, +} from '.'; +import { OptimizelyError } from '../../error/optimizly_error'; + +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); +var testData = getTestProjectConfig(); + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +describe('lib/core/bucketer', function () { + describe('APIs', function () { + describe('bucket', function () { + var configObj; + var createdLogger = createLogger(); + var bucketerParams; + + beforeEach(function () { + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + }); + + afterEach(function () { + createdLogger.info.restore(); + createdLogger.debug.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + }); + + describe('return values for bucketing (excluding groups)', function () { + beforeEach(function () { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: configObj.experiments[0].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + validateEntity: true, + }; + sinon + .stub(bucketValueGenerator, 'generateBucketValue') + .onFirstCall() + .returns(50) + .onSecondCall() + .returns(50000); + }); + + afterEach(function () { + bucketValueGenerator.generateBucketValue.restore(); + }); + + it('should return decision response with correct variation ID when provided bucket value', function () { + var bucketerParamsTest1 = cloneDeep(bucketerParams); + bucketerParamsTest1.userId = 'ppid1'; + var decisionResponse = bucketer.bucket(bucketerParamsTest1); + expect(decisionResponse.result).to.equal('111128'); + + expect(createdLogger.debug.args[0]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 50, 'ppid1']); + + var bucketerParamsTest2 = cloneDeep(bucketerParams); + bucketerParamsTest2.userId = 'ppid2'; + expect(bucketer.bucket(bucketerParamsTest2).result).to.equal(null); + + expect(createdLogger.debug.args[1]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 50000, 'ppid2']); + }); + }); + + describe('return values for bucketing (including groups)', function () { + var bucketerStub; + beforeEach(function () { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: configObj.experiments[0].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + validateEntity: true, + }; + bucketerStub = sinon.stub(bucketValueGenerator, 'generateBucketValue'); + }); + + afterEach(function () { + bucketValueGenerator.generateBucketValue.restore(); + }); + + describe('random groups', function () { + bucketerParams = {}; + beforeEach(function () { + bucketerParams = { + experimentId: configObj.experiments[4].id, + experimentKey: configObj.experiments[4].key, + trafficAllocationConfig: configObj.experiments[4].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + userId: 'testUser', + validateEntity: true, + }; + }); + + it('should return decision response with the proper variation for a user in a grouped experiment', function () { + bucketerStub.onFirstCall().returns(50); + bucketerStub.onSecondCall().returns(50); + + var decisionResponse = bucketer.bucket(bucketerParams); + expect(decisionResponse.result).to.equal('551'); + + sinon.assert.calledTwice(bucketerStub); + sinon.assert.callCount(createdLogger.debug, 2); + sinon.assert.callCount(createdLogger.info, 1); + + expect(createdLogger.debug.args[0]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 50, 'testUser']); + + expect(createdLogger.info.args[0]).to.deep.equal([USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, 'testUser', 'groupExperiment1', '666']); + + expect(createdLogger.debug.args[1]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 50, 'testUser']); + }); + + it('should return decision response with variation null when a user is bucketed into a different grouped experiment than the one speicfied', function () { + bucketerStub.returns(5000); + + var decisionResponse = bucketer.bucket(bucketerParams); + expect(decisionResponse.result).to.equal(null); + + sinon.assert.calledOnce(bucketerStub); + sinon.assert.calledOnce(createdLogger.debug); + sinon.assert.calledOnce(createdLogger.info); + + expect(createdLogger.debug.args[0]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 5000, 'testUser']); + + expect(createdLogger.info.args[0]).to.deep.equal([USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, 'testUser', 'groupExperiment1', '666']); + }); + + it('should return decision response with variation null when a user is not bucketed into any experiments in the random group', function () { + bucketerStub.returns(50000); + + var decisionResponse = bucketer.bucket(bucketerParams); + expect(decisionResponse.result).to.equal(null); + + sinon.assert.calledOnce(bucketerStub); + sinon.assert.calledOnce(createdLogger.debug); + sinon.assert.calledOnce(createdLogger.info); + + expect(createdLogger.debug.args[0]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 50000, 'testUser']); + + expect(createdLogger.info.args[0]).to.deep.equal([USER_NOT_IN_ANY_EXPERIMENT, 'testUser', '666']); + }); + + it('should return decision response with variation null when a user is bucketed into traffic space of deleted experiment within a random group', function () { + bucketerStub.returns(9000); + + var decisionResponse = bucketer.bucket(bucketerParams); + expect(decisionResponse.result).to.equal(null); + + sinon.assert.calledOnce(bucketerStub); + sinon.assert.calledOnce(createdLogger.debug); + sinon.assert.calledOnce(createdLogger.info); + + expect(createdLogger.debug.args[0]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 9000, 'testUser']); + + expect(createdLogger.info.args[0]).to.deep.equal([USER_NOT_IN_ANY_EXPERIMENT, 'testUser', '666']); + }); + + it('should throw an error if group ID is not in the datafile', function () { + var bucketerParamsWithInvalidGroupId = cloneDeep(bucketerParams); + bucketerParamsWithInvalidGroupId.experimentIdMap[configObj.experiments[4].id].groupId = '6969'; + + const ex = assert.throws(function () { + bucketer.bucket(bucketerParamsWithInvalidGroupId); + }); + assert.equal(ex.baseMessage, INVALID_GROUP_ID); + assert.deepEqual(ex.params, ['6969']); + }); + }); + + describe('overlapping groups', function () { + bucketerParams = {}; + beforeEach(function () { + bucketerParams = { + experimentId: configObj.experiments[6].id, + experimentKey: configObj.experiments[6].key, + trafficAllocationConfig: configObj.experiments[6].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + userId: 'testUser', + validateEntity: true, + }; + }); + + it('should return decision response with variation when a user falls into an experiment within an overlapping group', function () { + bucketerStub.returns(0); + + var decisionResponse = bucketer.bucket(bucketerParams); + expect(decisionResponse.result).to.equal('553'); + + sinon.assert.calledOnce(bucketerStub); + sinon.assert.calledOnce(createdLogger.debug); + + expect(createdLogger.debug.args[0]).to.deep.equal([USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 0, 'testUser']); + }); + + it('should return decision response with variation null when a user does not fall into an experiment within an overlapping group', function () { + bucketerStub.returns(3000); + + var decisionResponse = bucketer.bucket(bucketerParams); + expect(decisionResponse.result).to.equal(null); + }); + }); + }); + + describe('when the bucket value falls into empty traffic allocation ranges', function () { + beforeEach(function () { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: [ + { + entityId: '', + endOfRange: 5000, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + validateEntity: true, + }; + }); + + it('should return decision response with variation null', function () { + var bucketerParamsTest1 = cloneDeep(bucketerParams); + bucketerParamsTest1.userId = 'ppid1'; + var decisionResponse = bucketer.bucket(bucketerParamsTest1); + expect(decisionResponse.result).to.equal(null); + }); + + it('should not log an invalid variation ID warning', function () { + bucketer.bucket(bucketerParams) + const calls = [ + ...createdLogger.debug.getCalls(), + ...createdLogger.info.getCalls(), + ...createdLogger.warn.getCalls(), + ...createdLogger.error.getCalls(), + ]; + + const foundInvalidVariationWarning = calls.some((call) => { + const message = call.args[0]; + return message.includes('Bucketed into an invalid variation ID') + }); + expect(foundInvalidVariationWarning).to.equal(false); + }); + }); + + describe('when the traffic allocation has invalid variation ids', function () { + beforeEach(function () { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + bucketerParams = { + experimentId: configObj.experiments[0].id, + experimentKey: configObj.experiments[0].key, + trafficAllocationConfig: [ + { + entityId: -1, + endOfRange: 5000, + }, + { + entityId: -2, + endOfRange: 10000, + }, + ], + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + validateEntity: true, + }; + }); + + it('should return decision response with variation null', function () { + var bucketerParamsTest1 = cloneDeep(bucketerParams); + bucketerParamsTest1.userId = 'ppid1'; + var decisionResponse = bucketer.bucket(bucketerParamsTest1); + expect(decisionResponse.result).to.equal(null); + }); + }); + }); + + describe('generateBucketValue', function () { + it('should return a bucket value for different inputs', function () { + var experimentId = 1886780721; + var bucketingKey1 = sprintf('%s%s', 'ppid1', experimentId); + var bucketingKey2 = sprintf('%s%s', 'ppid2', experimentId); + var bucketingKey3 = sprintf('%s%s', 'ppid2', 1886780722); + var bucketingKey4 = sprintf('%s%s', 'ppid3', experimentId); + + expect(bucketValueGenerator.generateBucketValue(bucketingKey1)).to.equal(5254); + expect(bucketValueGenerator.generateBucketValue(bucketingKey2)).to.equal(4299); + expect(bucketValueGenerator.generateBucketValue(bucketingKey3)).to.equal(2434); + expect(bucketValueGenerator.generateBucketValue(bucketingKey4)).to.equal(5439); + }); + + it('should return an error if it cannot generate the hash value', function() { + const response = assert.throws(function() { + bucketValueGenerator.generateBucketValue(null); + } ); + expect(response.baseMessage).to.equal(INVALID_BUCKETING_ID); + }); + }); + + describe('testBucketWithBucketingId', function () { + var bucketerParams; + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + + beforeEach(function () { + var configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + bucketerParams = { + trafficAllocationConfig: configObj.experiments[0].trafficAllocation, + variationIdMap: configObj.variationIdMap, + experimentIdMap: configObj.experimentIdMap, + groupIdMap: configObj.groupIdMap, + logger: createdLogger, + validateEntity: true, + }; + }); + + it('check that a non null bucketingId buckets a variation different than the one expected with userId', function () { + var bucketerParams1 = cloneDeep(bucketerParams); + bucketerParams1['userId'] = 'testBucketingIdControl'; + bucketerParams1['bucketingId'] = '123456789'; + bucketerParams1['experimentKey'] = 'testExperiment'; + bucketerParams1['experimentId'] = '111127'; + expect(bucketer.bucket(bucketerParams1).result).to.equal('111129'); + }); + + it('check that a null bucketing ID defaults to bucketing with the userId', function () { + var bucketerParams2 = cloneDeep(bucketerParams); + bucketerParams2['userId'] = 'testBucketingIdControl'; + bucketerParams2['bucketingId'] = null; + bucketerParams2['experimentKey'] = 'testExperiment'; + bucketerParams2['experimentId'] = '111127'; + expect(bucketer.bucket(bucketerParams2).result).to.equal('111128'); + }); + + it('check that bucketing works with an experiment in group', function () { + var bucketerParams4 = cloneDeep(bucketerParams); + bucketerParams4['userId'] = 'testBucketingIdControl'; + bucketerParams4['bucketingId'] = '123456789'; + bucketerParams4['experimentKey'] = 'groupExperiment2'; + bucketerParams4['experimentId'] = '443'; + expect(bucketer.bucket(bucketerParams4).result).to.equal('111128'); + }); + }); + }); +}); diff --git a/lib/core/bucketer/index.ts b/lib/core/bucketer/index.ts new file mode 100644 index 000000000..e31c8df4b --- /dev/null +++ b/lib/core/bucketer/index.ts @@ -0,0 +1,210 @@ +/** + * Copyright 2016, 2019-2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Bucketer API for determining the variation id from the specified parameters + */ +import { LoggerFacade } from '../../logging/logger'; +import { + DecisionResponse, + BucketerParams, + TrafficAllocation, + Group, +} from '../../shared_types'; +import { INVALID_GROUP_ID } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { generateBucketValue } from './bucket_value_generator'; +import { DecisionReason } from '../decision_service'; + +export const USER_NOT_IN_ANY_EXPERIMENT = 'User %s is not in any experiment of group %s.'; +export const USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP = 'User %s is not in experiment %s of group %s.'; +export const USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP = 'User %s is in experiment %s of group %s.'; +export const USER_ASSIGNED_TO_EXPERIMENT_BUCKET = 'Assigned bucket %s to user with bucketing ID %s.'; +export const INVALID_VARIATION_ID = 'Bucketed into an invalid variation ID. Returning null.'; +const RANDOM_POLICY = 'random'; + +/** + * Determines ID of variation to be shown for the given input params + * @param {Object} bucketerParams + * @param {string} bucketerParams.experimentId + * @param {string} bucketerParams.experimentKey + * @param {string} bucketerParams.userId + * @param {Object[]} bucketerParams.trafficAllocationConfig + * @param {Array} bucketerParams.experimentKeyMap + * @param {Object} bucketerParams.groupIdMap + * @param {Object} bucketerParams.variationIdMap + * @param {string} bucketerParams.varationIdMap[].key + * @param {Object} bucketerParams.logger + * @param {string} bucketerParams.bucketingId + * @return {Object} DecisionResponse DecisionResponse containing variation ID that user has been bucketed into, + * null if user is not bucketed into any experiment and the decide reasons. + */ +export const bucket = function(bucketerParams: BucketerParams): DecisionResponse<string | null> { + const decideReasons: DecisionReason[] = []; + // Check if user is in a random group; if so, check if user is bucketed into a specific experiment + const experiment = bucketerParams.experimentIdMap[bucketerParams.experimentId]; + // Optional chaining skips groupId check for holdout experiments; Holdout experimentId is not in experimentIdMap + const groupId = experiment?.['groupId']; + if (groupId) { + const group = bucketerParams.groupIdMap[groupId]; + if (!group) { + throw new OptimizelyError(INVALID_GROUP_ID, groupId); + } + if (group.policy === RANDOM_POLICY) { + const bucketedExperimentId = bucketUserIntoExperiment( + group, + bucketerParams.bucketingId, + bucketerParams.userId, + bucketerParams.logger + ); + + // Return if user is not bucketed into any experiment + if (bucketedExperimentId === null) { + bucketerParams.logger?.info( + USER_NOT_IN_ANY_EXPERIMENT, + bucketerParams.userId, + groupId, + ); + decideReasons.push([ + USER_NOT_IN_ANY_EXPERIMENT, + bucketerParams.userId, + groupId, + ]); + return { + result: null, + reasons: decideReasons, + }; + } + + // Return if user is bucketed into a different experiment than the one specified + if (bucketedExperimentId !== bucketerParams.experimentId) { + bucketerParams.logger?.info( + USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + bucketerParams.userId, + bucketerParams.experimentKey, + groupId, + ); + decideReasons.push([ + USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + bucketerParams.userId, + bucketerParams.experimentKey, + groupId, + ]); + return { + result: null, + reasons: decideReasons, + }; + } + + // Continue bucketing if user is bucketed into specified experiment + bucketerParams.logger?.info( + USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + bucketerParams.userId, + bucketerParams.experimentKey, + groupId, + ); + decideReasons.push([ + USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, + bucketerParams.userId, + bucketerParams.experimentKey, + groupId, + ]); + } + } + const bucketingId = `${bucketerParams.bucketingId}${bucketerParams.experimentId}`; + const bucketValue = generateBucketValue(bucketingId); + + bucketerParams.logger?.debug( + USER_ASSIGNED_TO_EXPERIMENT_BUCKET, + bucketValue, + bucketerParams.userId, + ); + decideReasons.push([ + USER_ASSIGNED_TO_EXPERIMENT_BUCKET, + bucketValue, + bucketerParams.userId, + ]); + + const entityId = _findBucket(bucketValue, bucketerParams.trafficAllocationConfig); + + if (bucketerParams.validateEntity && entityId !== null && !bucketerParams.variationIdMap[entityId]) { + if (entityId) { + bucketerParams.logger?.warn(INVALID_VARIATION_ID); + decideReasons.push([INVALID_VARIATION_ID]); + } + return { + result: null, + reasons: decideReasons, + }; + } + + return { + result: entityId, + reasons: decideReasons, + }; +}; + +/** + * Returns bucketed experiment ID to compare against experiment user is being called into + * @param {Group} group Group that experiment is in + * @param {string} bucketingId Bucketing ID + * @param {string} userId ID of user to be bucketed into experiment + * @param {LoggerFacade} logger Logger implementation + * @return {string|null} ID of experiment if user is bucketed into experiment within the group, null otherwise + */ +export const bucketUserIntoExperiment = function( + group: Group, + bucketingId: string, + userId: string, + logger?: LoggerFacade +): string | null { + const bucketingKey = `${bucketingId}${group.id}`; + const bucketValue = generateBucketValue(bucketingKey); + logger?.debug( + USER_ASSIGNED_TO_EXPERIMENT_BUCKET, + bucketValue, + userId, + ); + const trafficAllocationConfig = group.trafficAllocation; + const bucketedExperimentId = _findBucket(bucketValue, trafficAllocationConfig); + return bucketedExperimentId; +}; + +/** + * Returns entity ID associated with bucket value + * @param {number} bucketValue + * @param {TrafficAllocation[]} trafficAllocationConfig + * @param {number} trafficAllocationConfig[].endOfRange + * @param {string} trafficAllocationConfig[].entityId + * @return {string|null} Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise + */ +export const _findBucket = function( + bucketValue: number, + trafficAllocationConfig: TrafficAllocation[] +): string | null { + for (let i = 0; i < trafficAllocationConfig.length; i++) { + if (bucketValue < trafficAllocationConfig[i].endOfRange) { + return trafficAllocationConfig[i].entityId; + } + } + + return null; +}; + +export default { + bucket: bucket, + bucketUserIntoExperiment: bucketUserIntoExperiment, +}; diff --git a/lib/core/condition_tree_evaluator/index.spec.ts b/lib/core/condition_tree_evaluator/index.spec.ts new file mode 100644 index 000000000..5afdd0d7d --- /dev/null +++ b/lib/core/condition_tree_evaluator/index.spec.ts @@ -0,0 +1,218 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, vi, expect } from 'vitest'; + +import * as conditionTreeEvaluator from '.'; + +const conditionA = { + name: 'browser_type', + value: 'safari', + type: 'custom_attribute', +}; +const conditionB = { + name: 'device_model', + value: 'iphone6', + type: 'custom_attribute', +}; +const conditionC = { + name: 'location', + match: 'exact', + type: 'custom_attribute', + value: 'CA', +}; +describe('evaluate', function() { + it('should return true for a leaf condition when the leaf condition evaluator returns true', function() { + expect( + conditionTreeEvaluator.evaluate(conditionA, function() { + return true; + }) + ).toBe(true); + }); + + it('should return false for a leaf condition when the leaf condition evaluator returns false', function() { + expect( + conditionTreeEvaluator.evaluate(conditionA, function() { + return false; + }) + ).toBe(false); + }); + + describe('and evaluation', function() { + it('should return true when ALL conditions evaluate to true', function() { + expect( + conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], function() { + return true; + }) + ).toBe(true); + }); + + it('should return false if one condition evaluates to false', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => true).mockImplementationOnce(() => false); + expect(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)).toBe(false); + }); + + describe('null handling', function() { + it('should return null when all operands evaluate to null', function() { + expect( + conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], function() { + return null; + }) + ).toBeNull(); + }); + + it('should return null when operands evaluate to trues and nulls', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => true).mockImplementationOnce(() => null); + expect(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)).toBeNull(); + }); + + it('should return false when operands evaluate to falses and nulls', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => false).mockImplementationOnce(() => null); + expect(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)).toBe(false); + + leafEvaluator.mockReset(); + leafEvaluator.mockImplementationOnce(() => null).mockImplementationOnce(() => false); + expect(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)).toBe(false); + }); + + it('should return false when operands evaluate to trues, falses, and nulls', function() { + const leafEvaluator = vi.fn(); + leafEvaluator + .mockImplementationOnce(() => true) + .mockImplementationOnce(() => false) + .mockImplementationOnce(() => null); + expect(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB, conditionC], leafEvaluator)).toBe(false); + }); + }); + }); + + describe('or evaluation', function() { + it('should return true if any condition evaluates to true', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => false).mockImplementationOnce(() => true); + expect(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)).toBe(true); + }); + + it('should return false if all conditions evaluate to false', function() { + expect( + conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], function() { + return false; + }) + ).toBe(false); + }); + + describe('null handling', function() { + it('should return null when all operands evaluate to null', function() { + expect( + conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], function() { + return null; + }) + ).toBeNull(); + }); + + it('should return true when operands evaluate to trues and nulls', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => true).mockImplementationOnce(() => null); + expect(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)).toBe(true); + }); + + it('should return null when operands evaluate to falses and nulls', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => null).mockImplementationOnce(() => false); + expect(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)).toBeNull(); + + leafEvaluator.mockReset(); + leafEvaluator.mockImplementationOnce(() => false).mockImplementationOnce(() => null); + expect(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)).toBeNull(); + }); + + it('should return true when operands evaluate to trues, falses, and nulls', function() { + const leafEvaluator = vi.fn(); + leafEvaluator + .mockImplementationOnce(() => true) + .mockImplementationOnce(() => null) + .mockImplementationOnce(() => false); + expect(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB, conditionC], leafEvaluator)).toBe(true); + }); + }); + }); + + describe('not evaluation', function() { + it('should return true if the condition evaluates to false', function() { + expect( + conditionTreeEvaluator.evaluate(['not', conditionA], function() { + return false; + }) + ).toBe(true); + }); + + it('should return false if the condition evaluates to true', function() { + expect( + conditionTreeEvaluator.evaluate(['not', conditionB], function() { + return true; + }) + ).toBe(false); + }); + + it('should return the result of negating the first condition, and ignore any additional conditions', function() { + let result = conditionTreeEvaluator.evaluate(['not', '1', '2', '1'], function(id: string) { + return id === '1'; + }); + expect(result).toBe(false); + result = conditionTreeEvaluator.evaluate(['not', '1', '2', '1'], function(id: string) { + return id === '2'; + }); + expect(result).toBe(true); + result = conditionTreeEvaluator.evaluate(['not', '1', '2', '3'], function(id: string) { + return id === '1' ? null : id === '3'; + }); + expect(result).toBeNull(); + }); + + describe('null handling', function() { + it('should return null when operand evaluates to null', function() { + expect( + conditionTreeEvaluator.evaluate(['not', conditionA], function() { + return null; + }) + ).toBeNull(); + }); + + it('should return null when there are no operands', function() { + expect( + conditionTreeEvaluator.evaluate(['not'], function() { + return null; + }) + ).toBeNull(); + }); + }); + }); + + describe('implicit operator', function() { + it('should behave like an "or" operator when the first item in the array is not a recognized operator', function() { + const leafEvaluator = vi.fn(); + leafEvaluator.mockImplementationOnce(() => true).mockImplementationOnce(() => false); + expect(conditionTreeEvaluator.evaluate([conditionA, conditionB], leafEvaluator)).toBe(true); + expect( + conditionTreeEvaluator.evaluate([conditionA, conditionB], function() { + return false; + }) + ).toBe(false); + }); + }); +}); diff --git a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.tests.js b/lib/core/condition_tree_evaluator/index.tests.js similarity index 61% rename from packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.tests.js rename to lib/core/condition_tree_evaluator/index.tests.js index 22ca3c6e0..e01c0f9f0 100644 --- a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.tests.js +++ b/lib/core/condition_tree_evaluator/index.tests.js @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2018, Optimizely, Inc. and contributors * + * Copyright 2018, 2020-2021, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ +import sinon from 'sinon'; +import { assert } from 'chai'; -var chai = require('chai'); -var sinon = require('sinon'); -var assert = chai.assert; -var conditionTreeEvaluator = require('./'); +import * as conditionTreeEvaluator from './'; var conditionA = { name: 'browser_type', @@ -40,66 +39,63 @@ describe('lib/core/condition_tree_evaluator', function() { describe('APIs', function() { describe('evaluate', function() { it('should return true for a leaf condition when the leaf condition evaluator returns true', function() { - assert.isTrue(conditionTreeEvaluator.evaluate(conditionA, function() { return true; })); + assert.isTrue( + conditionTreeEvaluator.evaluate(conditionA, function() { + return true; + }) + ); }); it('should return false for a leaf condition when the leaf condition evaluator returns false', function() { - assert.isFalse(conditionTreeEvaluator.evaluate(conditionA, function() { return false; })); + assert.isFalse( + conditionTreeEvaluator.evaluate(conditionA, function() { + return false; + }) + ); }); describe('and evaluation', function() { it('should return true when ALL conditions evaluate to true', function() { - assert.isTrue(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB], - function() { return true; } - )); + assert.isTrue( + conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], function() { + return true; + }) + ); }); it('should return false if one condition evaluates to false', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(true); leafEvaluator.onCall(1).returns(false); - assert.isFalse(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB], - leafEvaluator - )); + assert.isFalse(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)); }); describe('null handling', function() { it('should return null when all operands evaluate to null', function() { - assert.isNull(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB], - function() { return null; } - )); + assert.isNull( + conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], function() { + return null; + }) + ); }); it('should return null when operands evaluate to trues and nulls', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(true); leafEvaluator.onCall(1).returns(null); - assert.isNull(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB], - leafEvaluator - )); + assert.isNull(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)); }); it('should return false when operands evaluate to falses and nulls', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(false); leafEvaluator.onCall(1).returns(null); - assert.isFalse(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB], - leafEvaluator - )); + assert.isFalse(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)); leafEvaluator.reset(); leafEvaluator.onCall(0).returns(null); leafEvaluator.onCall(1).returns(false); - assert.isFalse(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB], - leafEvaluator - )); - + assert.isFalse(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB], leafEvaluator)); }); it('should return false when operands evaluate to trues, falses, and nulls', function() { @@ -107,10 +103,7 @@ describe('lib/core/condition_tree_evaluator', function() { leafEvaluator.onCall(0).returns(true); leafEvaluator.onCall(1).returns(false); leafEvaluator.onCall(2).returns(null); - assert.isFalse(conditionTreeEvaluator.evaluate( - ['and', conditionA, conditionB, conditionC], - leafEvaluator - )); + assert.isFalse(conditionTreeEvaluator.evaluate(['and', conditionA, conditionB, conditionC], leafEvaluator)); }); }); }); @@ -120,53 +113,43 @@ describe('lib/core/condition_tree_evaluator', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(false); leafEvaluator.onCall(1).returns(true); - assert.isTrue(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB], - leafEvaluator - )); + assert.isTrue(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)); }); it('should return false if all conditions evaluate to false', function() { - assert.isFalse(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB], - function() { return false; } - )); + assert.isFalse( + conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], function() { + return false; + }) + ); }); describe('null handling', function() { it('should return null when all operands evaluate to null', function() { - assert.isNull(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB], - function() { return null; } - )); + assert.isNull( + conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], function() { + return null; + }) + ); }); it('should return true when operands evaluate to trues and nulls', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(true); leafEvaluator.onCall(1).returns(null); - assert.isTrue(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB], - leafEvaluator - )); + assert.isTrue(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)); }); it('should return null when operands evaluate to falses and nulls', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(null); leafEvaluator.onCall(1).returns(false); - assert.isNull(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB], - leafEvaluator - )); + assert.isNull(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)); leafEvaluator.reset(); leafEvaluator.onCall(0).returns(false); leafEvaluator.onCall(1).returns(null); - assert.isNull(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB], - leafEvaluator - )); + assert.isNull(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB], leafEvaluator)); }); it('should return true when operands evaluate to trues, falses, and nulls', function() { @@ -174,48 +157,58 @@ describe('lib/core/condition_tree_evaluator', function() { leafEvaluator.onCall(0).returns(true); leafEvaluator.onCall(1).returns(null); leafEvaluator.onCall(2).returns(false); - assert.isTrue(conditionTreeEvaluator.evaluate( - ['or', conditionA, conditionB, conditionC], - leafEvaluator - )); + assert.isTrue(conditionTreeEvaluator.evaluate(['or', conditionA, conditionB, conditionC], leafEvaluator)); }); }); }); describe('not evaluation', function() { it('should return true if the condition evaluates to false', function() { - assert.isTrue(conditionTreeEvaluator.evaluate(['not', conditionA], function() { return false; })); + assert.isTrue( + conditionTreeEvaluator.evaluate(['not', conditionA], function() { + return false; + }) + ); }); it('should return false if the condition evaluates to true', function() { - assert.isFalse(conditionTreeEvaluator.evaluate(['not', conditionB], function() { return true; })); + assert.isFalse( + conditionTreeEvaluator.evaluate(['not', conditionB], function() { + return true; + }) + ); }); it('should return the result of negating the first condition, and ignore any additional conditions', function() { - var result = conditionTreeEvaluator.evaluate( - ['not', '1', '2', '1'], - function(id) { return id === '1'; } - ); + var result = conditionTreeEvaluator.evaluate(['not', '1', '2', '1'], function(id) { + return id === '1'; + }); assert.isFalse(result); - result = conditionTreeEvaluator.evaluate( - ['not', '1', '2', '1'], - function(id) { return id === '2'; } - ); + result = conditionTreeEvaluator.evaluate(['not', '1', '2', '1'], function(id) { + return id === '2'; + }); assert.isTrue(result); - result = conditionTreeEvaluator.evaluate( - ['not', '1', '2', '3'], - function(id) { return id === '1' ? null : id === '3'; } - ); + result = conditionTreeEvaluator.evaluate(['not', '1', '2', '3'], function(id) { + return id === '1' ? null : id === '3'; + }); assert.isNull(result); }); describe('null handling', function() { it('should return null when operand evaluates to null', function() { - assert.isNull(conditionTreeEvaluator.evaluate(['not', conditionA], function() { return null; })); + assert.isNull( + conditionTreeEvaluator.evaluate(['not', conditionA], function() { + return null; + }) + ); }); it('should return null when there are no operands', function() { - assert.isNull(conditionTreeEvaluator.evaluate(['not'], function() { return null; })); + assert.isNull( + conditionTreeEvaluator.evaluate(['not'], function() { + return null; + }) + ); }); }); }); @@ -225,14 +218,12 @@ describe('lib/core/condition_tree_evaluator', function() { var leafEvaluator = sinon.stub(); leafEvaluator.onCall(0).returns(true); leafEvaluator.onCall(1).returns(false); - assert.isTrue(conditionTreeEvaluator.evaluate( - [conditionA, conditionB], - leafEvaluator - )); - assert.isFalse(conditionTreeEvaluator.evaluate( - [conditionA, conditionB], - function() { return false; } - )); + assert.isTrue(conditionTreeEvaluator.evaluate([conditionA, conditionB], leafEvaluator)); + assert.isFalse( + conditionTreeEvaluator.evaluate([conditionA, conditionB], function() { + return false; + }) + ); }); }); }); diff --git a/lib/core/condition_tree_evaluator/index.ts b/lib/core/condition_tree_evaluator/index.ts new file mode 100644 index 000000000..7b0c8df9d --- /dev/null +++ b/lib/core/condition_tree_evaluator/index.ts @@ -0,0 +1,131 @@ +/**************************************************************************** + * Copyright 2018, 2021, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +const AND_CONDITION = 'and'; +const OR_CONDITION = 'or'; +const NOT_CONDITION = 'not'; + +export const DEFAULT_OPERATOR_TYPES = [AND_CONDITION, OR_CONDITION, NOT_CONDITION]; +export type ConditionTree<Leaf> = Leaf | unknown[]; + +type LeafEvaluator<Leaf> = (leaf: Leaf) => boolean | null; + +/** + * Top level method to evaluate conditions + * @param {ConditionTree<Leaf>} conditions Nested array of and/or conditions, or a single leaf + * condition value of any type + * Example: ['and', '0', ['or', '1', '2']] + * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition + * values + * @return {?boolean} Result of evaluating the conditions using the operator + * rules and the leaf evaluator. A return value of null + * indicates that the conditions are invalid or unable to be + * evaluated. + */ +export function evaluate<Leaf>(conditions: ConditionTree<Leaf>, leafEvaluator: LeafEvaluator<Leaf>): boolean | null { + if (Array.isArray(conditions)) { + let firstOperator = conditions[0]; + let restOfConditions = conditions.slice(1); + + if (typeof firstOperator === 'string' && DEFAULT_OPERATOR_TYPES.indexOf(firstOperator) === -1) { + // Operator to apply is not explicit - assume 'or' + firstOperator = OR_CONDITION; + restOfConditions = conditions; + } + + switch (firstOperator) { + case AND_CONDITION: + return andEvaluator(restOfConditions, leafEvaluator); + case NOT_CONDITION: + return notEvaluator(restOfConditions, leafEvaluator); + default: + // firstOperator is OR_CONDITION + return orEvaluator(restOfConditions, leafEvaluator); + } + } + + const leafCondition = conditions; + return leafEvaluator(leafCondition); +} + +/** + * Evaluates an array of conditions as if the evaluator had been applied + * to each entry and the results AND-ed together. + * @param {unknown[]} conditions Array of conditions ex: [operand_1, operand_2] + * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition values + * @return {?boolean} Result of evaluating the conditions. A return value of null + * indicates that the conditions are invalid or unable to be + * evaluated. + */ +function andEvaluator<Leaf>(conditions: ConditionTree<Leaf>, leafEvaluator: LeafEvaluator<Leaf>): boolean | null { + let sawNullResult = false; + if (Array.isArray(conditions)) { + for (let i = 0; i < conditions.length; i++) { + const conditionResult = evaluate(conditions[i] as ConditionTree<Leaf>, leafEvaluator); + if (conditionResult === false) { + return false; + } + if (conditionResult === null) { + sawNullResult = true; + } + } + return sawNullResult ? null : true; + } + return null; +} + +/** + * Evaluates an array of conditions as if the evaluator had been applied + * to a single entry and NOT was applied to the result. + * @param {unknown[]} conditions Array of conditions ex: [operand_1] + * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition values + * @return {?boolean} Result of evaluating the conditions. A return value of null + * indicates that the conditions are invalid or unable to be + * evaluated. + */ +function notEvaluator<Leaf>(conditions: ConditionTree<Leaf>, leafEvaluator: LeafEvaluator<Leaf>): boolean | null { + if (Array.isArray(conditions) && conditions.length > 0) { + const result = evaluate(conditions[0] as ConditionTree<Leaf>, leafEvaluator); + return result === null ? null : !result; + } + return null; +} + +/** + * Evaluates an array of conditions as if the evaluator had been applied + * to each entry and the results OR-ed together. + * @param {unknown[]} conditions Array of conditions ex: [operand_1, operand_2] + * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition values + * @return {?boolean} Result of evaluating the conditions. A return value of null + * indicates that the conditions are invalid or unable to be + * evaluated. + */ +function orEvaluator<Leaf>(conditions: ConditionTree<Leaf>, leafEvaluator: LeafEvaluator<Leaf>): boolean | null { + let sawNullResult = false; + if (Array.isArray(conditions)) { + for (let i = 0; i < conditions.length; i++) { + const conditionResult = evaluate(conditions[i] as ConditionTree<Leaf>, leafEvaluator); + if (conditionResult === true) { + return true; + } + if (conditionResult === null) { + sawNullResult = true; + } + } + return sawNullResult ? null : false; + } + return null; +} diff --git a/lib/core/custom_attribute_condition_evaluator/index.spec.ts b/lib/core/custom_attribute_condition_evaluator/index.spec.ts new file mode 100644 index 000000000..66f8cae0d --- /dev/null +++ b/lib/core/custom_attribute_condition_evaluator/index.spec.ts @@ -0,0 +1,1411 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import * as customAttributeEvaluator from './'; +import { MISSING_ATTRIBUTE_VALUE, UNEXPECTED_TYPE_NULL } from 'log_message'; +import { UNKNOWN_MATCH_TYPE, UNEXPECTED_TYPE, OUT_OF_BOUNDS, UNEXPECTED_CONDITION_VALUE } from 'error_message'; +import { Condition } from '../../shared_types'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { LoggerFacade } from '../../logging/logger'; + +const browserConditionSafari = { + name: 'browser_type', + value: 'safari', + type: 'custom_attribute', +}; +const booleanCondition = { + name: 'is_firefox', + value: true, + type: 'custom_attribute', +}; +const integerCondition = { + name: 'num_users', + value: 10, + type: 'custom_attribute', +}; +const doubleCondition = { + name: 'pi_value', + value: 3.14, + type: 'custom_attribute', +}; + +const getMockUserContext: any = (attributes: any) => ({ + getAttributes: () => ({ ...(attributes || {}) }), +}); + +const setLogSpy = (logger: LoggerFacade) => { + vi.spyOn(logger, 'error'); + vi.spyOn(logger, 'debug'); + vi.spyOn(logger, 'info'); + vi.spyOn(logger, 'warn'); +}; + +describe('custom_attribute_condition_evaluator', () => { + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true when the attributes pass the audience conditions and no match type is provided', () => { + const userAttributes = { + browser_type: 'safari', + }; + + expect( + customAttributeEvaluator.getEvaluator().evaluate(browserConditionSafari, getMockUserContext(userAttributes)) + ).toBe(true); + }); + + it('should return false when the attributes do not pass the audience conditions and no match type is provided', () => { + const userAttributes = { + browser_type: 'firefox', + }; + + expect( + customAttributeEvaluator.getEvaluator().evaluate(browserConditionSafari, getMockUserContext(userAttributes)) + ).toBe(false); + }); + + it('should evaluate different typed attributes', () => { + const userAttributes = { + browser_type: 'safari', + is_firefox: true, + num_users: 10, + pi_value: 3.14, + }; + + expect( + customAttributeEvaluator.getEvaluator().evaluate(browserConditionSafari, getMockUserContext(userAttributes)) + ).toBe(true); + expect(customAttributeEvaluator.getEvaluator().evaluate(booleanCondition, getMockUserContext(userAttributes))).toBe( + true + ); + expect(customAttributeEvaluator.getEvaluator().evaluate(integerCondition, getMockUserContext(userAttributes))).toBe( + true + ); + expect(customAttributeEvaluator.getEvaluator().evaluate(doubleCondition, getMockUserContext(userAttributes))).toBe( + true + ); + }); + + it('should log and return null when condition has an invalid match property', () => { + const invalidMatchCondition = { match: 'weird', name: 'weird_condition', type: 'custom_attribute', value: 'hi' }; + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidMatchCondition, getMockUserContext({ weird_condition: 'bye' })); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(UNKNOWN_MATCH_TYPE, JSON.stringify(invalidMatchCondition)); + }); +}); + +describe('exists match type', () => { + const existsCondition = { + match: 'exists', + name: 'input_value', + type: 'custom_attribute', + value: '', + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return false if there is no user-provided value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(existsCondition, getMockUserContext({})); + + expect(result).toBe(false); + expect(mockLogger.debug).not.toHaveBeenCalled(); + expect(mockLogger.info).not.toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it('should return false if the user-provided value is undefined', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(existsCondition, getMockUserContext({ input_value: undefined })); + + expect(result).toBe(false); + }); + + it('should return false if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(existsCondition, getMockUserContext({ input_value: null })); + + expect(result).toBe(false); + }); + + it('should return true if the user-provided value is a string', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(existsCondition, getMockUserContext({ input_value: 'hi' })); + + expect(result).toBe(true); + }); + + it('should return true if the user-provided value is a number', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(existsCondition, getMockUserContext({ input_value: 10 })); + + expect(result).toBe(true); + }); + + it('should return true if the user-provided value is a boolean', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(existsCondition, getMockUserContext({ input_value: true })); + + expect(result).toBe(true); + }); +}); + +describe('exact match type - with a string condition value', () => { + const exactStringCondition = { + match: 'exact', + name: 'favorite_constellation', + type: 'custom_attribute', + value: 'Lacerta', + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided value is equal to the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(exactStringCondition, getMockUserContext({ favorite_constellation: 'Lacerta' })); + + expect(result).toBe(true); + }); + + it('should return false if the user-provided value is not equal to the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator() + .evaluate(exactStringCondition, getMockUserContext({ favorite_constellation: 'The Big Dipper' })); + + expect(result).toBe(false); + }); + + it('should log and return null if condition value is of an unexpected type', () => { + const invalidExactCondition = { + match: 'exact', + name: 'favorite_constellation', + type: 'custom_attribute', + value: null, + }; + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidExactCondition, getMockUserContext({ favorite_constellation: 'Lacerta' })); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidExactCondition)); + }); + + it('should log and return null if the user-provided value is of a different type than the condition value', () => { + const unexpectedTypeUserAttributes: Record<string, boolean> = { favorite_constellation: false }; + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactStringCondition, getMockUserContext(unexpectedTypeUserAttributes)); + const userValue = unexpectedTypeUserAttributes[exactStringCondition.name]; + const userValueType = typeof userValue; + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(exactStringCondition), + userValueType, + exactStringCondition.name + ); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactStringCondition, getMockUserContext({ favorite_constellation: null })); + + expect(result).toBe(null); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith( + UNEXPECTED_TYPE_NULL, + JSON.stringify(exactStringCondition), + exactStringCondition.name + ); + }); + + it('should log and return null if there is no user-provided value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactStringCondition, getMockUserContext({})); + + expect(result).toBe(null); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith( + MISSING_ATTRIBUTE_VALUE, + JSON.stringify(exactStringCondition), + exactStringCondition.name + ); + }); + + it('should log and return null if the user-provided value is of an unexpected type', () => { + const unexpectedTypeUserAttributes: Record<string, unknown> = { favorite_constellation: [] }; + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactStringCondition, getMockUserContext(unexpectedTypeUserAttributes)); + const userValue = unexpectedTypeUserAttributes[exactStringCondition.name]; + const userValueType = typeof userValue; + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(exactStringCondition), + userValueType, + exactStringCondition.name + ); + }); +}); + +describe('exact match type - with a number condition value', () => { + const exactNumberCondition = { + match: 'exact', + name: 'lasers_count', + type: 'custom_attribute', + value: 9000, + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided value is equal to the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext({ lasers_count: 9000 })); + + expect(result).toBe(true); + }); + + it('should return false if the user-provided value is not equal to the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext({ lasers_count: 8000 })); + + expect(result).toBe(false); + }); + + it('should log and return null if the user-provided value is of a different type than the condition value', () => { + const unexpectedTypeUserAttributes1: Record<string, any> = { lasers_count: 'yes' }; + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext(unexpectedTypeUserAttributes1)); + + expect(result).toBe(null); + + const unexpectedTypeUserAttributes2: Record<string, any> = { lasers_count: '1000' }; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext(unexpectedTypeUserAttributes2)); + + expect(result).toBe(null); + + const userValue1 = unexpectedTypeUserAttributes1[exactNumberCondition.name]; + const userValueType1 = typeof userValue1; + const userValue2 = unexpectedTypeUserAttributes2[exactNumberCondition.name]; + const userValueType2 = typeof userValue2; + + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(exactNumberCondition), + userValueType1, + exactNumberCondition.name + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(exactNumberCondition), + userValueType2, + exactNumberCondition.name + ); + }); + + it('should log and return null if the user-provided number value is out of bounds', () => { + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext({ lasers_count: -Infinity })); + + expect(result).toBe(null); + + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext({ lasers_count: -Math.pow(2, 53) - 2 })); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + OUT_OF_BOUNDS, + JSON.stringify(exactNumberCondition), + exactNumberCondition.name + ); + }); + + it('should return null if there is no user-provided value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactNumberCondition, getMockUserContext({})); + + expect(result).toBe(null); + }); + + it('should log and return null if the condition value is not finite', () => { + const invalidValueCondition1 = { + match: 'exact', + name: 'lasers_count', + type: 'custom_attribute', + value: Infinity, + }; + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition1, getMockUserContext({ lasers_count: 9000 })); + + expect(result).toBe(null); + + const invalidValueCondition2 = { + match: 'exact', + name: 'lasers_count', + type: 'custom_attribute', + value: Math.pow(2, 53) + 2, + }; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition2, getMockUserContext({ lasers_count: 9000 })); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith(UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition1)); + expect(mockLogger.warn).toHaveBeenCalledWith(UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition2)); + }); +}); + +describe('exact match type - with a boolean condition value', () => { + const exactBoolCondition = { + match: 'exact', + name: 'did_register_user', + type: 'custom_attribute', + value: false, + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided value is equal to the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactBoolCondition, getMockUserContext({ did_register_user: false })); + + expect(result).toBe(true); + }); + + it('should return false if the user-provided value is not equal to the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactBoolCondition, getMockUserContext({ did_register_user: true })); + + expect(result).toBe(false); + }); + + it('should return null if the user-provided value is of a different type than the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactBoolCondition, getMockUserContext({ did_register_user: 10 })); + + expect(result).toBe(null); + }); + + it('should return null if there is no user-provided value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(exactBoolCondition, getMockUserContext({})); + + expect(result).toBe(null); + }); +}); + +describe('substring match type', () => { + const mockLogger = getMockLogger(); + const substringCondition = { + match: 'substring', + name: 'headline_text', + type: 'custom_attribute', + value: 'buy now', + }; + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the condition value is a substring of the user-provided value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + substringCondition, + getMockUserContext({ + headline_text: 'Limited time, buy now!', + }) + ); + + expect(result).toBe(true); + }); + + it('should return false if the user-provided value is not a substring of the condition value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + substringCondition, + getMockUserContext({ + headline_text: 'Breaking news!', + }) + ); + + expect(result).toBe(false); + }); + + it('should log and return null if the user-provided value is not a string', () => { + const unexpectedTypeUserAttributes: Record<string, unknown> = { headline_text: 10 }; + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(substringCondition, getMockUserContext(unexpectedTypeUserAttributes)); + const userValue = unexpectedTypeUserAttributes[substringCondition.name]; + const userValueType = typeof userValue; + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(substringCondition), + userValueType, + substringCondition.name + ); + }); + + it('should log and return null if the condition value is not a string', () => { + const nonStringCondition = { + match: 'substring', + name: 'headline_text', + type: 'custom_attribute', + value: 10, + }; + + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(nonStringCondition, getMockUserContext({ headline_text: 'hello' })); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(UNEXPECTED_CONDITION_VALUE, JSON.stringify(nonStringCondition)); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(substringCondition, getMockUserContext({ headline_text: null })); + + expect(result).toBe(null); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith( + UNEXPECTED_TYPE_NULL, + JSON.stringify(substringCondition), + substringCondition.name + ); + }); + + it('should return null if there is no user-provided value', function() { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(substringCondition, getMockUserContext({})); + + expect(result).toBe(null); + }); +}); + +describe('greater than match type', () => { + const gtCondition = { + match: 'gt', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided value is greater than the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext({ meters_travelled: 58.4 })); + + expect(result).toBe(true); + }); + + it('should return false if the user-provided value is not greater than the condition value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext({ meters_travelled: 20 })); + + expect(result).toBe(false); + }); + + it('should log and return null if the user-provided value is not a number', () => { + const unexpectedTypeUserAttributes1 = { meters_travelled: 'a long way' }; + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext(unexpectedTypeUserAttributes1)); + + expect(result).toBeNull(); + + const unexpectedTypeUserAttributes2 = { meters_travelled: '1000' }; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext(unexpectedTypeUserAttributes2)); + + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(gtCondition), + 'string', + gtCondition.name + ); + }); + + it('should log and return null if the user-provided number value is out of bounds', () => { + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext({ meters_travelled: -Infinity })); + + expect(result).toBeNull(); + + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext({ meters_travelled: Math.pow(2, 53) + 2 })); + + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith(OUT_OF_BOUNDS, JSON.stringify(gtCondition), gtCondition.name); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(gtCondition, getMockUserContext({ meters_travelled: null })); + + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(UNEXPECTED_TYPE_NULL, JSON.stringify(gtCondition), gtCondition.name); + }); + + it('should return null if there is no user-provided value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(gtCondition, getMockUserContext({})); + + expect(result).toBeNull(); + }); + + it('should return null if the condition value is not a finite number', () => { + const userAttributes = { meters_travelled: 58.4 }; + const invalidValueCondition: Condition = { + match: 'gt', + name: 'meters_travelled', + type: 'custom_attribute', + value: Infinity, + }; + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + + expect(result).toBeNull(); + + invalidValueCondition.value = null; + + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + + expect(result).toBeNull(); + + invalidValueCondition.value = Math.pow(2, 53) + 2; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(3); + expect(mockLogger.warn).toHaveBeenCalledWith(UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition)); + }); +}); + +describe('less than match type', () => { + const ltCondition = { + match: 'lt', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided value is less than the condition value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext({ + meters_travelled: 10, + }) + ); + + expect(result).toBe(true); + }); + + it('should return false if the user-provided value is not less than the condition value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext({ + meters_travelled: 64.64, + }) + ); + + expect(result).toBe(false); + }); + + it('should log and return null if the user-provided value is not a number', () => { + const unexpectedTypeUserAttributes1: Record<string, unknown> = { meters_travelled: true }; + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(ltCondition, getMockUserContext(unexpectedTypeUserAttributes1)); + + expect(result).toBeNull(); + + const unexpectedTypeUserAttributes2: Record<string, unknown> = { meters_travelled: '48.2' }; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(ltCondition, getMockUserContext(unexpectedTypeUserAttributes2)); + + expect(result).toBeNull(); + + const userValue1 = unexpectedTypeUserAttributes1[ltCondition.name]; + const userValueType1 = typeof userValue1; + const userValue2 = unexpectedTypeUserAttributes2[ltCondition.name]; + const userValueType2 = typeof userValue2; + + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(ltCondition), + userValueType1, + ltCondition.name + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(ltCondition), + userValueType2, + ltCondition.name + ); + }); + + it('should log and return null if the user-provided number value is out of bounds', () => { + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(ltCondition, getMockUserContext({ meters_travelled: Infinity })); + + expect(result).toBeNull(); + + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(ltCondition, getMockUserContext({ meters_travelled: Math.pow(2, 53) + 2 })); + + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith(OUT_OF_BOUNDS, JSON.stringify(ltCondition), ltCondition.name); + expect(mockLogger.warn).toHaveBeenCalledWith(OUT_OF_BOUNDS, JSON.stringify(ltCondition), ltCondition.name); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(ltCondition, getMockUserContext({ meters_travelled: null })); + + expect(result).toBeNull(); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith(UNEXPECTED_TYPE_NULL, JSON.stringify(ltCondition), ltCondition.name); + }); + + it('should return null if there is no user-provided value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(ltCondition, getMockUserContext({})); + + expect(result).toBeNull(); + }); + + it('should return null if the condition value is not a finite number', () => { + const userAttributes = { meters_travelled: 10 }; + const invalidValueCondition: Condition = { + match: 'lt', + name: 'meters_travelled', + type: 'custom_attribute', + value: Infinity, + }; + + let result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + + expect(result).toBeNull(); + + invalidValueCondition.value = null; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + + expect(result).toBeNull(); + + invalidValueCondition.value = Math.pow(2, 53) + 2; + result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + + expect(result).toBeNull(); + expect(mockLogger.warn).toHaveBeenCalledTimes(3); + expect(mockLogger.warn).toHaveBeenCalledWith(UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition)); + }); +}); + +describe('less than or equal match type', () => { + const leCondition = { + match: 'le', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return false if the user-provided value is greater than the condition value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + leCondition, + getMockUserContext({ + meters_travelled: 48.3, + }) + ); + + expect(result).toBe(false); + }); + + it('should return true if the user-provided value is less than or equal to the condition value', () => { + const versions = [48, 48.2]; + for (const userValue of versions) { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + leCondition, + getMockUserContext({ + meters_travelled: userValue, + }) + ); + + expect(result).toBe(true); + } + }); +}); + +describe('greater than and equal to match type', () => { + const geCondition = { + match: 'ge', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return false if the user-provided value is less than the condition value', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + geCondition, + getMockUserContext({ + meters_travelled: 48, + }) + ); + + expect(result).toBe(false); + }); + + it('should return true if the user-provided value is less than or equal to the condition value', () => { + const versions = [100, 48.2]; + versions.forEach(userValue => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + geCondition, + getMockUserContext({ + meters_travelled: userValue, + }) + ); + + expect(result).toBe(true); + }); + }); +}); + +describe('semver greater than match type', () => { + const semvergtCondition = { + match: 'semver_gt', + name: 'app_version', + type: 'custom_attribute', + value: '2.0.0', + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided version is greater than the condition version', () => { + const versions = [['1.8.1', '1.9']]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvergtCondition = { + match: 'semver_gt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvergtCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(true); + }); + }); + + it('should return false if the user-provided version is not greater than the condition version', function() { + const versions = [ + ['2.0.1', '2.0.1'], + ['2.0', '2.0.0'], + ['2.0', '2.0.1'], + ['2.0.1', '2.0.0'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvergtCondition = { + match: 'semver_gt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvergtCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(false); + }); + }); + + it('should log and return null if the user-provided version is not a string', () => { + let result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvergtCondition, + getMockUserContext({ + app_version: 22, + }) + ); + + expect(result).toBe(null); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvergtCondition, + getMockUserContext({ + app_version: false, + }) + ); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(semvergtCondition), + 'number', + 'app_version' + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(semvergtCondition), + 'boolean', + 'app_version' + ); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(semvergtCondition, getMockUserContext({ app_version: null })); + + expect(result).toBe(null); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith( + UNEXPECTED_TYPE_NULL, + JSON.stringify(semvergtCondition), + 'app_version' + ); + }); + + it('should return null if there is no user-provided value', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(semvergtCondition, getMockUserContext({})); + + expect(result).toBe(null); + }); +}); + +describe('semver less than match type', () => { + const semverltCondition = { + match: 'semver_lt', + name: 'app_version', + type: 'custom_attribute', + value: '2.0.0', + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return false if the user-provided version is greater than the condition version', () => { + const versions = [ + ['2.0.0', '2.0.1'], + ['1.9', '2.0.0'], + ['2.0.0', '2.0.0'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemverltCondition = { + match: 'semver_lt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemverltCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(false); + }); + }); + + it('should return true if the user-provided version is less than the condition version', () => { + const versions = [ + ['2.0.1', '2.0.0'], + ['2.0.0', '1.9'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemverltCondition = { + match: 'semver_lt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemverltCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(true); + }); + }); + + it('should log and return null if the user-provided version is not a string', () => { + let result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semverltCondition, + getMockUserContext({ + app_version: 22, + }) + ); + + expect(result).toBe(null); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semverltCondition, + getMockUserContext({ + app_version: false, + }) + ); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(semverltCondition), + 'number', + 'app_version' + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(semverltCondition), + 'boolean', + 'app_version' + ); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(semverltCondition, getMockUserContext({ app_version: null })); + + expect(result).toBe(null); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith( + UNEXPECTED_TYPE_NULL, + JSON.stringify(semverltCondition), + 'app_version' + ); + }); + + it('should return null if there is no user-provided value', function() { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(semverltCondition, getMockUserContext({})); + + expect(result).toBe(null); + }); +}); +describe('semver equal to match type', () => { + const semvereqCondition = { + match: 'semver_eq', + name: 'app_version', + type: 'custom_attribute', + value: '2.0', + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return false if the user-provided version is greater than the condition version', () => { + const versions = [ + ['2.0.0', '2.0.1'], + ['2.0.1', '2.0.0'], + ['1.9.1', '1.9'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvereqCondition = { + match: 'semver_eq', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(false); + }); + }); + + it('should return true if the user-provided version is equal to the condition version', () => { + const versions = [ + ['2.0.1', '2.0.1'], + ['1.9', '1.9.1'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvereqCondition = { + match: 'semver_eq', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(true); + }); + }); + + it('should log and return null if the user-provided version is not a string', () => { + let result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvereqCondition, + getMockUserContext({ + app_version: 22, + }) + ); + + expect(result).toBe(null); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvereqCondition, + getMockUserContext({ + app_version: false, + }) + ); + + expect(result).toBe(null); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(semvereqCondition), + 'number', + 'app_version' + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_TYPE, + JSON.stringify(semvereqCondition), + 'boolean', + 'app_version' + ); + }); + + it('should log and return null if the user-provided value is null', () => { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(semvereqCondition, getMockUserContext({ app_version: null })); + + expect(result).toBe(null); + expect(mockLogger.debug).toHaveBeenCalledTimes(1); + expect(mockLogger.debug).toHaveBeenCalledWith( + UNEXPECTED_TYPE_NULL, + JSON.stringify(semvereqCondition), + 'app_version' + ); + }); + + it('should return null if there is no user-provided value', function() { + const result = customAttributeEvaluator + .getEvaluator(mockLogger) + .evaluate(semvereqCondition, getMockUserContext({})); + + expect(result).toBe(null); + }); +}); + +describe('semver less than or equal to match type', () => { + const semverleCondition = { + match: 'semver_le', + name: 'app_version', + type: 'custom_attribute', + value: '2.0.0', + }; + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return false if the user-provided version is greater than the condition version', () => { + const versions = [['2.0.0', '2.0.1']]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvereqCondition = { + match: 'semver_le', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(false); + }); + }); + + it('should return true if the user-provided version is less than or equal to the condition version', () => { + const versions = [ + ['2.0.1', '2.0.0'], + ['2.0.1', '2.0.1'], + ['1.9', '1.9.1'], + ['1.9.1', '1.9'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvereqCondition = { + match: 'semver_le', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(true); + }); + }); + + it('should return true if the user-provided version is equal to the condition version', () => { + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semverleCondition, + getMockUserContext({ + app_version: '2.0', + }) + ); + + expect(result).toBe(true); + }); +}); + +describe('semver greater than or equal to match type', () => { + const mockLogger = getMockLogger(); + + beforeEach(() => { + setLogSpy(mockLogger); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return true if the user-provided version is greater than or equal to the condition version', () => { + const versions = [ + ['2.0.0', '2.0.1'], + ['2.0.1', '2.0.1'], + ['1.9', '1.9.1'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvereqCondition = { + match: 'semver_ge', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(true); + }); + }); + + it('should return false if the user-provided version is less than the condition version', () => { + const versions = [ + ['2.0.1', '2.0.0'], + ['1.9.1', '1.9'], + ]; + versions.forEach(([targetVersion, userVersion]) => { + const customSemvereqCondition = { + match: 'semver_ge', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + const result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + + expect(result).toBe(false); + }); + }); +}); diff --git a/lib/core/custom_attribute_condition_evaluator/index.tests.js b/lib/core/custom_attribute_condition_evaluator/index.tests.js new file mode 100644 index 000000000..12607e001 --- /dev/null +++ b/lib/core/custom_attribute_condition_evaluator/index.tests.js @@ -0,0 +1,1126 @@ +/**************************************************************************** + * Copyright 2018-2020, 2022, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import sinon from 'sinon'; +import { assert } from 'chai'; +import { sprintf } from '../../utils/fns'; + +import * as customAttributeEvaluator from './'; +import { + MISSING_ATTRIBUTE_VALUE, + UNEXPECTED_TYPE_NULL, +} from 'log_message'; +import { + UNKNOWN_MATCH_TYPE, + UNEXPECTED_TYPE, + OUT_OF_BOUNDS, + UNEXPECTED_CONDITION_VALUE, +} from 'error_message'; + +var browserConditionSafari = { + name: 'browser_type', + value: 'safari', + type: 'custom_attribute', +}; +var booleanCondition = { + name: 'is_firefox', + value: true, + type: 'custom_attribute', +}; +var integerCondition = { + name: 'num_users', + value: 10, + type: 'custom_attribute', +}; +var doubleCondition = { + name: 'pi_value', + value: 3.14, + type: 'custom_attribute', +}; + +var getMockUserContext = (attributes) => ({ + getAttributes: () => ({ ... (attributes || {})}) +}); + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}); + +describe('lib/core/custom_attribute_condition_evaluator', function() { + var mockLogger = createLogger(); + + beforeEach(function() { + sinon.stub(mockLogger, 'error'); + sinon.stub(mockLogger, 'debug'); + sinon.stub(mockLogger, 'info'); + sinon.stub(mockLogger, 'warn'); + }); + + afterEach(function() { + mockLogger.error.restore(); + mockLogger.debug.restore(); + mockLogger.info.restore(); + mockLogger.warn.restore(); + }); + + it('should return true when the attributes pass the audience conditions and no match type is provided', function() { + var userAttributes = { + browser_type: 'safari', + }; + + assert.isTrue(customAttributeEvaluator.getEvaluator().evaluate(browserConditionSafari, getMockUserContext(userAttributes))); + }); + + it('should return false when the attributes do not pass the audience conditions and no match type is provided', function() { + var userAttributes = { + browser_type: 'firefox', + }; + + assert.isFalse(customAttributeEvaluator.getEvaluator().evaluate(browserConditionSafari, getMockUserContext(userAttributes))); + }); + + it('should evaluate different typed attributes', function() { + var userAttributes = { + browser_type: 'safari', + is_firefox: true, + num_users: 10, + pi_value: 3.14, + }; + + assert.isTrue(customAttributeEvaluator.getEvaluator().evaluate(browserConditionSafari, getMockUserContext(userAttributes))); + assert.isTrue(customAttributeEvaluator.getEvaluator().evaluate(booleanCondition, getMockUserContext(userAttributes))); + assert.isTrue(customAttributeEvaluator.getEvaluator().evaluate(integerCondition, getMockUserContext(userAttributes))); + assert.isTrue(customAttributeEvaluator.getEvaluator().evaluate(doubleCondition, getMockUserContext(userAttributes))); + }); + + it('should log and return null when condition has an invalid match property', function() { + var invalidMatchCondition = { match: 'weird', name: 'weird_condition', type: 'custom_attribute', value: 'hi' }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + invalidMatchCondition, + getMockUserContext({ weird_condition: 'bye' }) + ); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.warn); + assert.strictEqual(mockLogger.warn.args[0][0], UNKNOWN_MATCH_TYPE); + assert.strictEqual(mockLogger.warn.args[0][1], JSON.stringify(invalidMatchCondition)); + }); + + describe('exists match type', function() { + var existsCondition = { + match: 'exists', + name: 'input_value', + type: 'custom_attribute', + }; + + it('should return false if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(existsCondition, getMockUserContext({})); + assert.isFalse(result); + sinon.assert.notCalled(mockLogger.debug); + sinon.assert.notCalled(mockLogger.info); + sinon.assert.notCalled(mockLogger.warn); + sinon.assert.notCalled(mockLogger.error); + }); + + it('should return false if the user-provided value is undefined', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate(existsCondition, getMockUserContext({ input_value: undefined })); + assert.isFalse(result); + }); + + it('should return false if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate(existsCondition, getMockUserContext({ input_value: null })); + assert.isFalse(result); + }); + + it('should return true if the user-provided value is a string', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate(existsCondition, getMockUserContext({ input_value: 'hi' })); + assert.isTrue(result); + }); + + it('should return true if the user-provided value is a number', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate(existsCondition, getMockUserContext({ input_value: 10 })); + assert.isTrue(result); + }); + + it('should return true if the user-provided value is a boolean', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate(existsCondition, getMockUserContext({ input_value: true })); + assert.isTrue(result); + }); + }); + + describe('exact match type', function() { + describe('with a string condition value', function() { + var exactStringCondition = { + match: 'exact', + name: 'favorite_constellation', + type: 'custom_attribute', + value: 'Lacerta', + }; + + it('should return true if the user-provided value is equal to the condition value', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate( + exactStringCondition, + getMockUserContext({ favorite_constellation: 'Lacerta' }) + ); + assert.isTrue(result); + }); + + it('should return false if the user-provided value is not equal to the condition value', function() { + var result = customAttributeEvaluator.getEvaluator().evaluate( + exactStringCondition, + getMockUserContext({ favorite_constellation: 'The Big Dipper' }) + ); + assert.isFalse(result); + }); + + it('should log and return null if condition value is of an unexpected type', function() { + var invalidExactCondition = { + match: 'exact', + name: 'favorite_constellation', + type: 'custom_attribute', + value: [], + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + invalidExactCondition, + getMockUserContext({ favorite_constellation: 'Lacerta' }) + ); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.warn); + assert.strictEqual(mockLogger.warn.args[0][0], UNEXPECTED_CONDITION_VALUE); + assert.strictEqual(mockLogger.warn.args[0][1], JSON.stringify(invalidExactCondition)); + }); + + it('should log and return null if the user-provided value is of a different type than the condition value', function() { + var unexpectedTypeUserAttributes = { favorite_constellation: false }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + exactStringCondition, + getMockUserContext(unexpectedTypeUserAttributes) + ); + assert.isNull(result); + + var userValue = unexpectedTypeUserAttributes[exactStringCondition.name]; + var userValueType = typeof userValue; + sinon.assert.calledOnce(mockLogger.warn); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(exactStringCondition), userValueType, exactStringCondition.name]); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + exactStringCondition, + getMockUserContext({ favorite_constellation: null }) + ); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.deepEqual(mockLogger.debug.args[0], [UNEXPECTED_TYPE_NULL, JSON.stringify(exactStringCondition), exactStringCondition.name]); + }); + + it('should log and return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactStringCondition, getMockUserContext({})); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.deepEqual(mockLogger.debug.args[0], [MISSING_ATTRIBUTE_VALUE, JSON.stringify(exactStringCondition), exactStringCondition.name]); + }); + + it('should log and return null if the user-provided value is of an unexpected type', function() { + var unexpectedTypeUserAttributes = { favorite_constellation: [] }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + exactStringCondition, + getMockUserContext(unexpectedTypeUserAttributes) + ); + assert.isNull(result); + var userValue = unexpectedTypeUserAttributes[exactStringCondition.name]; + var userValueType = typeof userValue; + sinon.assert.calledOnce(mockLogger.warn); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(exactStringCondition), userValueType, exactStringCondition.name]); + }); + }); + + describe('with a number condition value', function() { + var exactNumberCondition = { + match: 'exact', + name: 'lasers_count', + type: 'custom_attribute', + value: 9000, + }; + + it('should return true if the user-provided value is equal to the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactNumberCondition, getMockUserContext({ lasers_count: 9000 })); + assert.isTrue(result); + }); + + it('should return false if the user-provided value is not equal to the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactNumberCondition, getMockUserContext({ lasers_count: 8000 })); + assert.isFalse(result); + }); + + it('should log and return null if the user-provided value is of a different type than the condition value', function() { + var unexpectedTypeUserAttributes1 = { lasers_count: 'yes' }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + exactNumberCondition, + getMockUserContext(unexpectedTypeUserAttributes1) + ); + assert.isNull(result); + + var unexpectedTypeUserAttributes2 = { lasers_count: '1000' }; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + exactNumberCondition, + getMockUserContext(unexpectedTypeUserAttributes2) + ); + assert.isNull(result); + + var userValue1 = unexpectedTypeUserAttributes1[exactNumberCondition.name]; + var userValueType1 = typeof userValue1; + var userValue2 = unexpectedTypeUserAttributes2[exactNumberCondition.name]; + var userValueType2 = typeof userValue2; + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(exactNumberCondition), userValueType1, exactNumberCondition.name]); + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_TYPE, JSON.stringify(exactNumberCondition), userValueType2, exactNumberCondition.name]); + }); + + it('should log and return null if the user-provided number value is out of bounds', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactNumberCondition, getMockUserContext({ lasers_count: -Infinity })); + assert.isNull(result); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + exactNumberCondition, + getMockUserContext({ lasers_count: -Math.pow(2, 53) - 2 }) + ); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [OUT_OF_BOUNDS, JSON.stringify(exactNumberCondition), exactNumberCondition.name]); + + assert.deepEqual(mockLogger.warn.args[1], [OUT_OF_BOUNDS, JSON.stringify(exactNumberCondition), exactNumberCondition.name]); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactNumberCondition, getMockUserContext({})); + assert.isNull(result); + }); + + it('should log and return null if the condition value is not finite', function() { + var invalidValueCondition1 = { + match: 'exact', + name: 'lasers_count', + type: 'custom_attribute', + value: Infinity, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition1, getMockUserContext({ lasers_count: 9000 })); + assert.isNull(result); + + var invalidValueCondition2 = { + match: 'exact', + name: 'lasers_count', + type: 'custom_attribute', + value: Math.pow(2, 53) + 2, + }; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition2, getMockUserContext({ lasers_count: 9000 })); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition1)]); + + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition2)]); + }); + }); + + describe('with a boolean condition value', function() { + var exactBoolCondition = { + match: 'exact', + name: 'did_register_user', + type: 'custom_attribute', + value: false, + }; + + it('should return true if the user-provided value is equal to the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactBoolCondition, getMockUserContext({ did_register_user: false })); + assert.isTrue(result); + }); + + it('should return false if the user-provided value is not equal to the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactBoolCondition, getMockUserContext({ did_register_user: true })); + assert.isFalse(result); + }); + + it('should return null if the user-provided value is of a different type than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactBoolCondition, getMockUserContext({ did_register_user: 10 })); + assert.isNull(result); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(exactBoolCondition, getMockUserContext({})); + assert.isNull(result); + }); + }); + }); + + describe('substring match type', function() { + var substringCondition = { + match: 'substring', + name: 'headline_text', + type: 'custom_attribute', + value: 'buy now', + }; + + it('should return true if the condition value is a substring of the user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + substringCondition, + getMockUserContext({ + headline_text: 'Limited time, buy now!', + }) + ); + assert.isTrue(result); + }); + + it('should return false if the user-provided value is not a substring of the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + substringCondition, + getMockUserContext({ + headline_text: 'Breaking news!', + }) + ); + assert.isFalse(result); + }); + + it('should log and return null if the user-provided value is not a string', function() { + var unexpectedTypeUserAttributes = { headline_text: 10 }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + substringCondition, + getMockUserContext(unexpectedTypeUserAttributes) + ); + assert.isNull(result); + var userValue = unexpectedTypeUserAttributes[substringCondition.name]; + var userValueType = typeof userValue; + sinon.assert.calledOnce(mockLogger.warn); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(substringCondition), userValueType, substringCondition.name]); + }); + + it('should log and return null if the condition value is not a string', function() { + var nonStringCondition = { + match: 'substring', + name: 'headline_text', + type: 'custom_attribute', + value: 10, + }; + + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(nonStringCondition, getMockUserContext({ headline_text: 'hello' })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.warn); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_CONDITION_VALUE, JSON.stringify(nonStringCondition)]); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(substringCondition, getMockUserContext({ headline_text: null })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.deepEqual(mockLogger.debug.args[0], [UNEXPECTED_TYPE_NULL, JSON.stringify(substringCondition), substringCondition.name]); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(substringCondition, getMockUserContext({})); + assert.isNull(result); + }); + }); + + describe('greater than match type', function() { + var gtCondition = { + match: 'gt', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + + it('should return true if the user-provided value is greater than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + gtCondition, + getMockUserContext({ + meters_travelled: 58.4, + }) + ); + assert.isTrue(result); + }); + + it('should return false if the user-provided value is not greater than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + gtCondition, + getMockUserContext({ + meters_travelled: 20, + }) + ); + assert.isFalse(result); + }); + + it('should log and return null if the user-provided value is not a number', function() { + var unexpectedTypeUserAttributes1 = { meters_travelled: 'a long way' }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + gtCondition, + getMockUserContext(unexpectedTypeUserAttributes1) + ); + assert.isNull(result); + + var unexpectedTypeUserAttributes2 = { meters_travelled: '1000' }; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + gtCondition, + getMockUserContext(unexpectedTypeUserAttributes2) + ); + assert.isNull(result); + + var userValue1 = unexpectedTypeUserAttributes1[gtCondition.name]; + var userValueType1 = typeof userValue1; + var userValue2 = unexpectedTypeUserAttributes2[gtCondition.name]; + var userValueType2 = typeof userValue2; + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(gtCondition), userValueType1, gtCondition.name]); + + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_TYPE, JSON.stringify(gtCondition), userValueType2, gtCondition.name]); + }); + + it('should log and return null if the user-provided number value is out of bounds', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + gtCondition, + getMockUserContext({ meters_travelled: -Infinity }) + ); + assert.isNull(result); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + gtCondition, + getMockUserContext({ meters_travelled: Math.pow(2, 53) + 2 }) + ); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [OUT_OF_BOUNDS, JSON.stringify(gtCondition), gtCondition.name]); + + assert.deepEqual(mockLogger.warn.args[1], [OUT_OF_BOUNDS, JSON.stringify(gtCondition), gtCondition.name]); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(gtCondition, getMockUserContext({ meters_travelled: null })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.deepEqual(mockLogger.debug.args[0], [UNEXPECTED_TYPE_NULL, JSON.stringify(gtCondition), gtCondition.name]); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(gtCondition, getMockUserContext({})); + assert.isNull(result); + }); + + it('should return null if the condition value is not a finite number', function() { + var userAttributes = { meters_travelled: 58.4 }; + var invalidValueCondition = { + match: 'gt', + name: 'meters_travelled', + type: 'custom_attribute', + value: Infinity, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + assert.isNull(result); + + invalidValueCondition.value = null; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + assert.isNull(result); + + invalidValueCondition.value = Math.pow(2, 53) + 2; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + assert.isNull(result); + + sinon.assert.calledThrice(mockLogger.warn); + + assert.deepEqual(mockLogger.warn.args[2], [UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition)]); + }); + }); + + describe('less than match type', function() { + var ltCondition = { + match: 'lt', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + + it('should return true if the user-provided value is less than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext({ + meters_travelled: 10, + }) + ); + assert.isTrue(result); + }); + + it('should return false if the user-provided value is not less than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext({ + meters_travelled: 64.64, + }) + ); + assert.isFalse(result); + }); + + it('should log and return null if the user-provided value is not a number', function() { + var unexpectedTypeUserAttributes1 = { meters_travelled: true }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext(unexpectedTypeUserAttributes1) + ); + assert.isNull(result); + + var unexpectedTypeUserAttributes2 = { meters_travelled: '48.2' }; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext(unexpectedTypeUserAttributes2) + ); + assert.isNull(result); + + var userValue1 = unexpectedTypeUserAttributes1[ltCondition.name]; + var userValueType1 = typeof userValue1; + var userValue2 = unexpectedTypeUserAttributes2[ltCondition.name]; + var userValueType2 = typeof userValue2; + + assert.strictEqual(2, mockLogger.warn.callCount); + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(ltCondition), userValueType1, ltCondition.name]); + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_TYPE, JSON.stringify(ltCondition), userValueType2, ltCondition.name]); + }); + + it('should log and return null if the user-provided number value is out of bounds', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext({ + meters_travelled: Infinity, + }) + ); + assert.isNull(result); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + ltCondition, + getMockUserContext({ + meters_travelled: Math.pow(2, 53) + 2, + }) + ); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [OUT_OF_BOUNDS, JSON.stringify(ltCondition), ltCondition.name]); + + assert.deepEqual(mockLogger.warn.args[1], [OUT_OF_BOUNDS, JSON.stringify(ltCondition), ltCondition.name]); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(ltCondition, getMockUserContext({ meters_travelled: null })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.deepEqual(mockLogger.debug.args[0], [UNEXPECTED_TYPE_NULL, JSON.stringify(ltCondition), ltCondition.name]); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(ltCondition, getMockUserContext({})); + assert.isNull(result); + }); + + it('should return null if the condition value is not a finite number', function() { + var userAttributes = { meters_travelled: 10 }; + var invalidValueCondition = { + match: 'lt', + name: 'meters_travelled', + type: 'custom_attribute', + value: Infinity, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + assert.isNull(result); + + invalidValueCondition.value = {}; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + assert.isNull(result); + + invalidValueCondition.value = Math.pow(2, 53) + 2; + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(invalidValueCondition, getMockUserContext(userAttributes)); + assert.isNull(result); + + sinon.assert.calledThrice(mockLogger.warn); + assert.deepEqual(mockLogger.warn.args[2], [UNEXPECTED_CONDITION_VALUE, JSON.stringify(invalidValueCondition)]); + }); + }); + describe('less than or equal to match type', function() { + var leCondition = { + match: 'le', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + + it('should return false if the user-provided value is greater than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + leCondition, + getMockUserContext({ + meters_travelled: 48.3, + }) + ); + assert.isFalse(result); + }); + + it('should return true if the user-provided value is less than or equal to the condition value', function() { + var versions = [48, 48.2]; + for (let userValue of versions) { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + leCondition, + getMockUserContext({ + meters_travelled: userValue, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for condition value: ${leCondition.value} and user value: ${userValue}`); + } + }); + }); + + + describe('greater than and equal to match type', function() { + var geCondition = { + match: 'ge', + name: 'meters_travelled', + type: 'custom_attribute', + value: 48.2, + }; + + it('should return false if the user-provided value is less than the condition value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + geCondition, + getMockUserContext({ + meters_travelled: 48, + }) + ); + assert.isFalse(result); + }); + + it('should return true if the user-provided value is less than or equal to the condition value', function() { + var versions = [100, 48.2]; + for (let userValue of versions) { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + geCondition, + getMockUserContext({ + meters_travelled: userValue, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for condition value: ${geCondition.value} and user value: ${userValue}`); + } + }); + }); + + describe('semver greater than match type', function() { + var semvergtCondition = { + match: 'semver_gt', + name: 'app_version', + type: 'custom_attribute', + value: '2.0.0', + }; + + it('should return true if the user-provided version is greater than the condition version', function() { + var versions = [ + ['1.8.1', '1.9'] + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemvergtCondition = { + match: 'semver_gt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvergtCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return false if the user-provided version is not greater than the condition version', function() { + var versions = [ + ['2.0.1', '2.0.1'], + ['2.0', '2.0.0'], + ['2.0', '2.0.1'], + ['2.0.1', '2.0.0'], + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemvergtCondition = { + match: 'semver_gt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvergtCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should log and return null if the user-provided version is not a string', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvergtCondition, + getMockUserContext({ + app_version: 22, + }) + ); + assert.isNull(result); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvergtCondition, + getMockUserContext({ + app_version: false, + }) + ); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(semvergtCondition), 'number', 'app_version']); + + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_TYPE, JSON.stringify(semvergtCondition), 'boolean', 'app_version']); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(semvergtCondition, getMockUserContext({ app_version: null })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.deepEqual(mockLogger.debug.args[0], [UNEXPECTED_TYPE_NULL, JSON.stringify(semvergtCondition), 'app_version']); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(semvergtCondition, getMockUserContext({})); + assert.isNull(result); + }); + }); + + describe('semver less than match type', function() { + var semverltCondition = { + match: 'semver_lt', + name: 'app_version', + type: 'custom_attribute', + value: '2.0.0', + }; + + it('should return false if the user-provided version is greater than the condition version', function() { + var versions = [ + ['2.0.0', '2.0.1'], + ['1.9', '2.0.0'], + ['2.0.0', '2.0.0'], + + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemverltCondition = { + match: 'semver_lt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemverltCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return true if the user-provided version is less than the condition version', function() { + var versions = [ + ['2.0.1', '2.0.0'], + ['2.0.0', '1.9'] + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemverltCondition = { + match: 'semver_lt', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemverltCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should log and return null if the user-provided version is not a string', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semverltCondition, + getMockUserContext({ + app_version: 22, + }) + ); + assert.isNull(result); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semverltCondition, + getMockUserContext({ + app_version: false, + }) + ); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(semverltCondition), 'number', 'app_version']); + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_TYPE, JSON.stringify(semverltCondition), 'boolean', 'app_version']); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(semverltCondition, getMockUserContext({ app_version: null })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + assert.deepEqual(mockLogger.debug.args[0], [UNEXPECTED_TYPE_NULL, JSON.stringify(semverltCondition), 'app_version']); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(semverltCondition, getMockUserContext({})); + assert.isNull(result); + }); + }); + + describe('semver equal to match type', function() { + var semvereqCondition = { + match: 'semver_eq', + name: 'app_version', + type: 'custom_attribute', + value: '2.0', + }; + + it('should return false if the user-provided version is greater than the condition version', function() { + var versions = [ + ['2.0.0', '2.0.1'], + ['2.0.1', '2.0.0'], + ['1.9.1', '1.9'] + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemvereqCondition = { + match: 'semver_eq', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return true if the user-provided version is equal to the condition version', function() { + var versions = [ + ['2.0.1', '2.0.1'], + ['1.9', '1.9.1'] + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemvereqCondition = { + match: 'semver_eq', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should log and return null if the user-provided version is not a string', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvereqCondition, + getMockUserContext({ + app_version: 22, + }) + ); + assert.isNull(result); + + result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semvereqCondition, + getMockUserContext({ + app_version: false, + }) + ); + assert.isNull(result); + + assert.strictEqual(2, mockLogger.warn.callCount); + assert.deepEqual(mockLogger.warn.args[0], [UNEXPECTED_TYPE, JSON.stringify(semvereqCondition), 'number', 'app_version']); + assert.deepEqual(mockLogger.warn.args[1], [UNEXPECTED_TYPE, JSON.stringify(semvereqCondition), 'boolean', 'app_version']); + }); + + it('should log and return null if the user-provided value is null', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(semvereqCondition, getMockUserContext({ app_version: null })); + assert.isNull(result); + sinon.assert.calledOnce(mockLogger.debug); + + assert.strictEqual(mockLogger.debug.args[0][0], UNEXPECTED_TYPE_NULL); + assert.strictEqual(mockLogger.debug.args[0][1], JSON.stringify(semvereqCondition)); + }); + + it('should return null if there is no user-provided value', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate(semvereqCondition, getMockUserContext({})); + assert.isNull(result); + }); + }); + + describe('semver less than or equal to match type', function() { + var semverleCondition = { + match: 'semver_le', + name: 'app_version', + type: 'custom_attribute', + value: '2.0.0', + }; + + it('should return false if the user-provided version is greater than the condition version', function() { + var versions = [ + ['2.0.0', '2.0.1'] + ] + for (let [targetVersion, userVersion] of versions) { + var customSemvereqCondition = { + match: 'semver_le', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return true if the user-provided version is less than or equal to the condition version', function() { + var versions = [ + ['2.0.1', '2.0.0'], + ['2.0.1', '2.0.1'], + ['1.9', '1.9.1'], + ['1.9.1', '1.9'], + ]; for (let [targetVersion, userVersion] of versions) { + var customSemvereqCondition = { + match: 'semver_le', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return true if the user-provided version is equal to the condition version', function() { + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + semverleCondition, + getMockUserContext({ + app_version: '2.0', + }) + ); + assert.isTrue(result); + }); + }); + + describe('semver greater than or equal to match type', function() { + var semvergeCondition = { + match: 'semver_ge', + name: 'app_version', + type: 'custom_attribute', + value: '2.0', + }; + + it('should return true if the user-provided version is greater than or equal to the condition version', function() { + var versions = [ + ['2.0.0', '2.0.1'], + ['2.0.1', '2.0.1'], + ['1.9', '1.9.1'] + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemvereqCondition = { + match: 'semver_ge', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return false if the user-provided version is less than the condition version', function() { + var versions = [ + ['2.0.1', '2.0.0'], + ['1.9.1', '1.9'] + ]; + for (let [targetVersion, userVersion] of versions) { + var customSemvereqCondition = { + match: 'semver_ge', + name: 'app_version', + type: 'custom_attribute', + value: targetVersion, + }; + var result = customAttributeEvaluator.getEvaluator(mockLogger).evaluate( + customSemvereqCondition, + getMockUserContext({ + app_version: userVersion, + }) + ); + assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + }); +}); diff --git a/lib/core/custom_attribute_condition_evaluator/index.ts b/lib/core/custom_attribute_condition_evaluator/index.ts new file mode 100644 index 000000000..797a7d4e0 --- /dev/null +++ b/lib/core/custom_attribute_condition_evaluator/index.ts @@ -0,0 +1,480 @@ +/**************************************************************************** + * Copyright 2018-2019, 2020, 2022, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import { Condition, OptimizelyUserContext } from '../../shared_types'; + +import fns from '../../utils/fns'; +import { compareVersion } from '../../utils/semantic_version'; +import { + MISSING_ATTRIBUTE_VALUE, + UNEXPECTED_TYPE_NULL, +} from 'log_message'; +import { + OUT_OF_BOUNDS, + UNEXPECTED_TYPE, + UNEXPECTED_CONDITION_VALUE, + UNKNOWN_MATCH_TYPE +} from 'error_message'; +import { LoggerFacade } from '../../logging/logger'; + +const EXACT_MATCH_TYPE = 'exact'; +const EXISTS_MATCH_TYPE = 'exists'; +const GREATER_OR_EQUAL_THAN_MATCH_TYPE = 'ge'; +const GREATER_THAN_MATCH_TYPE = 'gt'; +const LESS_OR_EQUAL_THAN_MATCH_TYPE = 'le'; +const LESS_THAN_MATCH_TYPE = 'lt'; +const SEMVER_EXACT_MATCH_TYPE = 'semver_eq'; +const SEMVER_GREATER_OR_EQUAL_THAN_MATCH_TYPE = 'semver_ge'; +const SEMVER_GREATER_THAN_MATCH_TYPE = 'semver_gt'; +const SEMVER_LESS_OR_EQUAL_THAN_MATCH_TYPE = 'semver_le'; +const SEMVER_LESS_THAN_MATCH_TYPE = 'semver_lt'; +const SUBSTRING_MATCH_TYPE = 'substring'; + +const MATCH_TYPES = [ + EXACT_MATCH_TYPE, + EXISTS_MATCH_TYPE, + GREATER_THAN_MATCH_TYPE, + GREATER_OR_EQUAL_THAN_MATCH_TYPE, + LESS_THAN_MATCH_TYPE, + LESS_OR_EQUAL_THAN_MATCH_TYPE, + SUBSTRING_MATCH_TYPE, + SEMVER_EXACT_MATCH_TYPE, + SEMVER_LESS_THAN_MATCH_TYPE, + SEMVER_LESS_OR_EQUAL_THAN_MATCH_TYPE, + SEMVER_GREATER_THAN_MATCH_TYPE, + SEMVER_GREATER_OR_EQUAL_THAN_MATCH_TYPE +]; + +type ConditionEvaluator = (condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade) => boolean | null; +type Evaluator = { evaluate: (condition: Condition, user: OptimizelyUserContext) => boolean | null; } + +const EVALUATORS_BY_MATCH_TYPE: { [conditionType: string]: ConditionEvaluator | undefined } = {}; +EVALUATORS_BY_MATCH_TYPE[EXACT_MATCH_TYPE] = exactEvaluator; +EVALUATORS_BY_MATCH_TYPE[EXISTS_MATCH_TYPE] = existsEvaluator; +EVALUATORS_BY_MATCH_TYPE[GREATER_THAN_MATCH_TYPE] = greaterThanEvaluator; +EVALUATORS_BY_MATCH_TYPE[GREATER_OR_EQUAL_THAN_MATCH_TYPE] = greaterThanOrEqualEvaluator; +EVALUATORS_BY_MATCH_TYPE[LESS_THAN_MATCH_TYPE] = lessThanEvaluator; +EVALUATORS_BY_MATCH_TYPE[LESS_OR_EQUAL_THAN_MATCH_TYPE] = lessThanOrEqualEvaluator; +EVALUATORS_BY_MATCH_TYPE[SUBSTRING_MATCH_TYPE] = substringEvaluator; +EVALUATORS_BY_MATCH_TYPE[SEMVER_EXACT_MATCH_TYPE] = semverEqualEvaluator; +EVALUATORS_BY_MATCH_TYPE[SEMVER_GREATER_THAN_MATCH_TYPE] = semverGreaterThanEvaluator; +EVALUATORS_BY_MATCH_TYPE[SEMVER_GREATER_OR_EQUAL_THAN_MATCH_TYPE] = semverGreaterThanOrEqualEvaluator; +EVALUATORS_BY_MATCH_TYPE[SEMVER_LESS_THAN_MATCH_TYPE] = semverLessThanEvaluator; +EVALUATORS_BY_MATCH_TYPE[SEMVER_LESS_OR_EQUAL_THAN_MATCH_TYPE] = semverLessThanOrEqualEvaluator; + +export const getEvaluator = (logger?: LoggerFacade): Evaluator => { + return { + evaluate(condition: Condition, user: OptimizelyUserContext): boolean | null { + return evaluate(condition, user, logger); + } + }; +} + +/** + * Given a custom attribute audience condition and user attributes, evaluate the + * condition against the attributes. + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @return {?boolean} true/false if the given user attributes match/don't match the given condition, + * null if the given user attributes and condition can't be evaluated + * TODO: Change to accept and object with named properties + */ +function evaluate(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const conditionMatch = condition.match; + if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { + logger?.warn(UNKNOWN_MATCH_TYPE, JSON.stringify(condition)); + return null; + } + + const attributeKey = condition.name; + if (!userAttributes.hasOwnProperty(attributeKey) && conditionMatch != EXISTS_MATCH_TYPE) { + logger?.debug( + MISSING_ATTRIBUTE_VALUE, JSON.stringify(condition), attributeKey + ); + return null; + } + + let evaluatorForMatch; + if (!conditionMatch) { + evaluatorForMatch = exactEvaluator; + } else { + evaluatorForMatch = EVALUATORS_BY_MATCH_TYPE[conditionMatch] || exactEvaluator; + } + + return evaluatorForMatch(condition, user, logger); +} + +/** + * Returns true if the value is valid for exact conditions. Valid values include + * strings, booleans, and numbers that aren't NaN, -Infinity, or Infinity. + * @param value + * @returns {boolean} + */ +function isValueTypeValidForExactConditions(value: unknown): boolean { + return typeof value === 'string' || typeof value === 'boolean' || fns.isNumber(value); +} + +/** + * Evaluate the given exact match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @return {?boolean} true if the user attribute value is equal (===) to the condition value, + * false if the user attribute value is not equal (!==) to the condition value, + * null if the condition value or user attribute value has an invalid type, or + * if there is a mismatch between the user attribute type and the condition value + * type + */ +function exactEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const conditionValue = condition.value; + const conditionValueType = typeof conditionValue; + const conditionName = condition.name; + const userValue = userAttributes[conditionName]; + const userValueType = typeof userValue; + + if ( + !isValueTypeValidForExactConditions(conditionValue) || + (fns.isNumber(conditionValue) && !fns.isSafeInteger(conditionValue)) + ) { + logger?.warn( + UNEXPECTED_CONDITION_VALUE, JSON.stringify(condition) + ); + return null; + } + + if (userValue === null) { + logger?.debug( + UNEXPECTED_TYPE_NULL, JSON.stringify(condition), conditionName + ); + return null; + } + + if (!isValueTypeValidForExactConditions(userValue) || conditionValueType !== userValueType) { + logger?.warn( + UNEXPECTED_TYPE, JSON.stringify(condition), userValueType, conditionName + ); + return null; + } + + if (fns.isNumber(userValue) && !fns.isSafeInteger(userValue)) { + logger?.warn( + OUT_OF_BOUNDS, JSON.stringify(condition), conditionName + ); + return null; + } + + return conditionValue === userValue; +} + +/** + * Evaluate the given exists match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {boolean} true if both: + * 1) the user attributes have a value for the given condition, and + * 2) the user attribute value is neither null nor undefined + * Returns false otherwise + */ +function existsEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean { + const userAttributes = user.getAttributes(); + const userValue = userAttributes[condition.name]; + return typeof userValue !== 'undefined' && userValue !== null; +} + +/** + * Validate user and condition values + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?boolean} true if values are valid, + * false if values are not valid + */ +function validateValuesForNumericCondition(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean { + const userAttributes = user.getAttributes(); + const conditionName = condition.name; + const userValue = userAttributes[conditionName]; + const userValueType = typeof userValue; + const conditionValue = condition.value; + + if (conditionValue === null || !fns.isSafeInteger(conditionValue)) { + logger?.warn( + UNEXPECTED_CONDITION_VALUE, JSON.stringify(condition) + ); + return false; + } + + if (userValue === null) { + logger?.debug( + UNEXPECTED_TYPE_NULL, JSON.stringify(condition), conditionName + ); + return false; + } + + if (!fns.isNumber(userValue)) { + logger?.warn( + UNEXPECTED_TYPE, JSON.stringify(condition), userValueType, conditionName + ); + return false; + } + + if (!fns.isSafeInteger(userValue)) { + logger?.warn( + OUT_OF_BOUNDS, JSON.stringify(condition), conditionName + ); + return false; + } + return true; +} + +/** + * Evaluate the given greater than match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?boolean} true if the user attribute value is greater than the condition value, + * false if the user attribute value is less than or equal to the condition value, + * null if the condition value isn't a number or the user attribute value + * isn't a number + */ +function greaterThanEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const userValue = userAttributes[condition.name]; + const conditionValue = condition.value; + + if (!validateValuesForNumericCondition(condition, user, logger) || conditionValue === null) { + return null; + } + return userValue! > conditionValue; +} + +/** + * Evaluate the given greater or equal than match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute value is greater or equal than the condition value, + * false if the user attribute value is less than to the condition value, + * null if the condition value isn't a number or the user attribute value isn't a + * number + */ +function greaterThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const userValue = userAttributes[condition.name]; + const conditionValue = condition.value; + + if (!validateValuesForNumericCondition(condition, user, logger) || conditionValue === null) { + return null; + } + + return userValue! >= conditionValue; +} + +/** + * Evaluate the given less than match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?boolean} true if the user attribute value is less than the condition value, + * false if the user attribute value is greater than or equal to the condition value, + * null if the condition value isn't a number or the user attribute value isn't a + * number + */ +function lessThanEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const userValue = userAttributes[condition.name]; + const conditionValue = condition.value; + + if (!validateValuesForNumericCondition(condition, user, logger) || conditionValue === null) { + return null; + } + + return userValue! < conditionValue; +} + +/** + * Evaluate the given less or equal than match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute value is less or equal than the condition value, + * false if the user attribute value is greater than to the condition value, + * null if the condition value isn't a number or the user attribute value isn't a + * number + */ +function lessThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const userValue = userAttributes[condition.name]; + const conditionValue = condition.value; + + if (!validateValuesForNumericCondition(condition, user, logger) || conditionValue === null) { + return null; + } + + return userValue! <= conditionValue; +} + +/** + * Evaluate the given substring match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the condition value is a substring of the user attribute value, + * false if the condition value is not a substring of the user attribute value, + * null if the condition value isn't a string or the user attribute value + * isn't a string + */ +function substringEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const userAttributes = user.getAttributes(); + const conditionName = condition.name; + const userValue = userAttributes[condition.name]; + const userValueType = typeof userValue; + const conditionValue = condition.value; + + if (typeof conditionValue !== 'string') { + logger?.warn( + UNEXPECTED_CONDITION_VALUE, JSON.stringify(condition) + ); + return null; + } + + if (userValue === null) { + logger?.debug( + UNEXPECTED_TYPE_NULL, JSON.stringify(condition), conditionName + ); + return null; + } + + if (typeof userValue !== 'string') { + logger?.warn( + UNEXPECTED_TYPE, JSON.stringify(condition), userValueType, conditionName + ); + return null; + } + + return userValue.indexOf(conditionValue) !== -1; +} + +/** + * Evaluate the given semantic version match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?number} returns compareVersion result + * null if the user attribute version has an invalid type + */ +function evaluateSemanticVersion(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): number | null { + const userAttributes = user.getAttributes(); + const conditionName = condition.name; + const userValue = userAttributes[conditionName]; + const userValueType = typeof userValue; + const conditionValue = condition.value; + + if (typeof conditionValue !== 'string') { + logger?.warn( + UNEXPECTED_CONDITION_VALUE, JSON.stringify(condition) + ); + return null; + } + + if (userValue === null) { + logger?.debug( + UNEXPECTED_TYPE_NULL, JSON.stringify(condition), conditionName + ); + return null; + } + + if (typeof userValue !== 'string') { + logger?.warn( + UNEXPECTED_TYPE, JSON.stringify(condition), userValueType, conditionName + ); + return null; + } + + return compareVersion(conditionValue, userValue, logger); +} + +/** + * Evaluate the given version match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is equal (===) to the condition version, + * false if the user attribute version is not equal (!==) to the condition version, + * null if the user attribute version has an invalid type + */ +function semverEqualEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const result = evaluateSemanticVersion(condition, user, logger); + if (result === null) { + return null; + } + return result === 0; +} + +/** + * Evaluate the given version match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is greater (>) than the condition version, + * false if the user attribute version is not greater than the condition version, + * null if the user attribute version has an invalid type + */ +function semverGreaterThanEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const result = evaluateSemanticVersion(condition, user, logger); + if (result === null) { + return null; + } + return result > 0; +} + +/** + * Evaluate the given version match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is less (<) than the condition version, + * false if the user attribute version is not less than the condition version, + * null if the user attribute version has an invalid type + */ +function semverLessThanEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const result = evaluateSemanticVersion(condition, user, logger); + if (result === null) { + return null; + } + return result < 0; +} + +/** + * Evaluate the given version match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is greater than or equal (>=) to the condition version, + * false if the user attribute version is not greater than or equal to the condition version, + * null if the user attribute version has an invalid type + */ +function semverGreaterThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const result = evaluateSemanticVersion(condition, user, logger); + if (result === null) { + return null; + } + return result >= 0; +} + +/** + * Evaluate the given version match condition for the given user attributes + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is less than or equal (<=) to the condition version, + * false if the user attribute version is not less than or equal to the condition version, + * null if the user attribute version has an invalid type + */ +function semverLessThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext, logger?: LoggerFacade): boolean | null { + const result = evaluateSemanticVersion(condition, user, logger); + if (result === null) { + return null; + } + return result <= 0; +} diff --git a/lib/core/decision/index.spec.ts b/lib/core/decision/index.spec.ts new file mode 100644 index 000000000..ea98fba39 --- /dev/null +++ b/lib/core/decision/index.spec.ts @@ -0,0 +1,128 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from 'vitest'; +import { rolloutDecisionObj, featureTestDecisionObj } from '../../tests/test_data'; +import * as decision from './'; + +describe('getExperimentKey method', () => { + it('should return empty string when experiment is null', () => { + const experimentKey = decision.getExperimentKey(rolloutDecisionObj); + + expect(experimentKey).toEqual(''); + }); + + it('should return empty string when experiment is not defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const experimentKey = decision.getExperimentKey({}); + + expect(experimentKey).toEqual(''); + }); + + it('should return experiment key when experiment is defined', () => { + const experimentKey = decision.getExperimentKey(featureTestDecisionObj); + + expect(experimentKey).toEqual('testing_my_feature'); + }); +}); + +describe('getExperimentId method', () => { + it('should return null when experiment is null', () => { + const experimentId = decision.getExperimentId(rolloutDecisionObj); + + expect(experimentId).toEqual(null); + }); + + it('should return null when experiment is not defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const experimentId = decision.getExperimentId({}); + + expect(experimentId).toEqual(null); + }); + + it('should return experiment id when experiment is defined', () => { + const experimentId = decision.getExperimentId(featureTestDecisionObj); + + expect(experimentId).toEqual('594098'); + }); + + describe('getVariationKey method', ()=> { + it('should return empty string when variation is null', () => { + const variationKey = decision.getVariationKey(rolloutDecisionObj); + + expect(variationKey).toEqual(''); + }); + + it('should return empty string when variation is not defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const variationKey = decision.getVariationKey({}); + + expect(variationKey).toEqual(''); + }); + + it('should return variation key when variation is defined', () => { + const variationKey = decision.getVariationKey(featureTestDecisionObj); + + expect(variationKey).toEqual('variation'); + }); + }); + + describe('getVariationId method', () => { + it('should return null when variation is null', () => { + const variationId = decision.getVariationId(rolloutDecisionObj); + + expect(variationId).toEqual(null); + }); + + it('should return null when variation is not defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const variationId = decision.getVariationId({}); + + expect(variationId).toEqual(null); + }); + + it('should return variation id when variation is defined', () => { + const variationId = decision.getVariationId(featureTestDecisionObj); + + expect(variationId).toEqual('594096'); + }); + }); + + describe('getFeatureEnabledFromVariation method', () => { + it('should return false when variation is null', () => { + const featureEnabled = decision.getFeatureEnabledFromVariation(rolloutDecisionObj); + + expect(featureEnabled).toEqual(false); + }); + + it('should return false when variation is not defined', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const featureEnabled = decision.getFeatureEnabledFromVariation({}); + + expect(featureEnabled).toEqual(false); + }); + + it('should return featureEnabled boolean when variation is defined', () => { + const featureEnabled = decision.getFeatureEnabledFromVariation(featureTestDecisionObj); + + expect(featureEnabled).toEqual(true); + }); + }); +}); diff --git a/lib/core/decision/index.tests.js b/lib/core/decision/index.tests.js new file mode 100644 index 000000000..3ae72b362 --- /dev/null +++ b/lib/core/decision/index.tests.js @@ -0,0 +1,105 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; +import { rolloutDecisionObj, featureTestDecisionObj } from '../../tests/test_data'; +import * as decision from './'; + +describe('lib/core/decision', function() { + describe('getExperimentKey method', function() { + it('should return empty string when experiment is null', function() { + var experimentKey = decision.getExperimentKey(rolloutDecisionObj); + assert.strictEqual(experimentKey, ''); + }); + + it('should return empty string when experiment is not defined', function() { + var experimentKey = decision.getExperimentKey({}); + assert.strictEqual(experimentKey, ''); + }); + + it('should return experiment key when experiment is defined', function() { + var experimentKey = decision.getExperimentKey(featureTestDecisionObj); + assert.strictEqual(experimentKey, 'testing_my_feature'); + }); + }); + + describe('getExperimentId method', function() { + it('should return null when experiment is null', function() { + var experimentId = decision.getExperimentId(rolloutDecisionObj); + assert.strictEqual(experimentId, null); + }); + + it('should return null when experiment is not defined', function() { + var experimentId = decision.getExperimentId({}); + assert.strictEqual(experimentId, null); + }); + + it('should return experiment id when experiment is defined', function() { + var experimentId = decision.getExperimentId(featureTestDecisionObj); + assert.strictEqual(experimentId, '594098'); + }); + }); + + describe('getVariationKey method', function() { + it('should return empty string when variation is null', function() { + var variationKey = decision.getVariationKey(rolloutDecisionObj); + assert.strictEqual(variationKey, ''); + }); + + it('should return empty string when variation is not defined', function() { + var variationKey = decision.getVariationKey({}); + assert.strictEqual(variationKey, ''); + }); + + it('should return variation key when variation is defined', function() { + var variationKey = decision.getVariationKey(featureTestDecisionObj); + assert.strictEqual(variationKey, 'variation'); + }); + }); + + describe('getVariationId method', function() { + it('should return null when variation is null', function() { + var variationId = decision.getVariationId(rolloutDecisionObj); + assert.strictEqual(variationId, null); + }); + + it('should return null when variation is not defined', function() { + var variationId = decision.getVariationId({}); + assert.strictEqual(variationId, null); + }); + + it('should return variation id when variation is defined', function() { + var variationId = decision.getVariationId(featureTestDecisionObj); + assert.strictEqual(variationId, '594096'); + }); + }); + + describe('getFeatureEnabledFromVariation method', function() { + it('should return false when variation is null', function() { + var featureEnabled = decision.getFeatureEnabledFromVariation(rolloutDecisionObj); + assert.strictEqual(featureEnabled, false); + }); + + it('should return false when variation is not defined', function() { + var featureEnabled = decision.getFeatureEnabledFromVariation({}); + assert.strictEqual(featureEnabled, false); + }); + + it('should return featureEnabled boolean when variation is defined', function() { + var featureEnabled = decision.getFeatureEnabledFromVariation(featureTestDecisionObj); + assert.strictEqual(featureEnabled, true); + }); + }); +}) diff --git a/lib/core/decision/index.ts b/lib/core/decision/index.ts new file mode 100644 index 000000000..27fd1c734 --- /dev/null +++ b/lib/core/decision/index.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DecisionObj } from '../decision_service'; + +/** + * Get experiment key from the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {string} Experiment key or empty string if experiment is null + */ +export function getExperimentKey(decisionObj: DecisionObj): string { + return decisionObj.experiment?.key ?? ''; +} + +/** + * Get variation key from the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {string} Variation key or empty string if variation is null + */ +export function getVariationKey(decisionObj: DecisionObj): string { + return decisionObj.variation?.key ?? ''; +} + +/** + * Get featureEnabled from variation in the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {boolean} featureEnabled boolean or false if variation is null + */ +export function getFeatureEnabledFromVariation(decisionObj: DecisionObj): boolean { + return decisionObj.variation?.featureEnabled ?? false; +} + +/** + * Get experiment id from the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {string} Experiment id or null if experiment is null + */ +export function getExperimentId(decisionObj: DecisionObj): string | null { + return decisionObj.experiment?.id ?? null; +} + +/** + * Get variation id from the provided decision object + * @param {DecisionObj} decisionObj Object representing decision + * @returns {string} Variation id or null if variation is null + */ +export function getVariationId(decisionObj: DecisionObj): string | null { + return decisionObj.variation?.id ?? null; +} diff --git a/lib/core/decision_service/cmab/cmab_client.spec.ts b/lib/core/decision_service/cmab/cmab_client.spec.ts new file mode 100644 index 000000000..04c7246ca --- /dev/null +++ b/lib/core/decision_service/cmab/cmab_client.spec.ts @@ -0,0 +1,357 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi, Mocked, Mock, MockInstance, beforeEach, afterEach } from 'vitest'; + +import { DefaultCmabClient } from './cmab_client'; +import { getMockAbortableRequest, getMockRequestHandler } from '../../../tests/mock/mock_request_handler'; +import { RequestHandler } from '../../../utils/http_request_handler/http'; +import { advanceTimersByTime, exhaustMicrotasks } from '../../../tests/testUtils'; +import { OptimizelyError } from '../../../error/optimizly_error'; + +const mockSuccessResponse = (variation: string) => Promise.resolve({ + statusCode: 200, + body: JSON.stringify({ + predictions: [ + { + variation_id: variation, + }, + ], + }), + headers: {} +}); + +const mockErrorResponse = (statusCode: number) => Promise.resolve({ + statusCode, + body: '', + headers: {}, +}); + +const assertRequest = ( + call: number, + mockRequestHandler: MockInstance<RequestHandler['makeRequest']>, + ruleId: string, + userId: string, + attributes: Record<string, any>, + cmabUuid: string, +) => { + const [requestUrl, headers, method, data] = mockRequestHandler.mock.calls[call]; + expect(requestUrl).toBe(`https://prediction.cmab.optimizely.com/predict/${ruleId}`); + expect(method).toBe('POST'); + expect(headers).toEqual({ + 'Content-Type': 'application/json', + }); + + const parsedData = JSON.parse(data!); + expect(parsedData.instances).toEqual([ + { + visitorId: userId, + experimentId: ruleId, + attributes: Object.keys(attributes).map((key) => ({ + id: key, + value: attributes[key], + type: 'custom_attribute', + })), + cmabUUID: cmabUuid, + } + ]); +}; + +describe('DefaultCmabClient', () => { + it('should fetch variation using correct parameters', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(mockSuccessResponse('var123'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + const variation = await cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); + + expect(variation).toBe('var123'); + assertRequest(0, mockMakeRequest, ruleId, userId, attributes, cmabUuid); + }); + + it('should retry fetch if retryConfig is provided', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValueOnce(getMockAbortableRequest(Promise.reject('error'))) + .mockReturnValueOnce(getMockAbortableRequest(mockErrorResponse(500))) + .mockReturnValueOnce(getMockAbortableRequest(mockSuccessResponse('var123'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + }, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + const variation = await cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); + + expect(variation).toBe('var123'); + expect(mockMakeRequest.mock.calls.length).toBe(3); + for(let i = 0; i < 3; i++) { + assertRequest(i, mockMakeRequest, ruleId, userId, attributes, cmabUuid); + } + }); + + it('should use backoff provider if provided', async () => { + vi.useFakeTimers(); + + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValueOnce(getMockAbortableRequest(Promise.reject('error'))) + .mockReturnValueOnce(getMockAbortableRequest(mockErrorResponse(500))) + .mockReturnValueOnce(getMockAbortableRequest(mockErrorResponse(500))) + .mockReturnValueOnce(getMockAbortableRequest(mockSuccessResponse('var123'))); + + const backoffProvider = () => { + let call = 0; + const values = [100, 200, 300]; + return { + reset: () => {}, + backoff: () => { + return values[call++]; + }, + }; + } + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + backoffProvider, + }, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + const fetchPromise = cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); + + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(1); + + // first backoff is 100ms, should not retry yet + await advanceTimersByTime(90); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(1); + + // first backoff is 100ms, should retry now + await advanceTimersByTime(10); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(2); + + // second backoff is 200ms, should not retry 2nd time yet + await advanceTimersByTime(150); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(2); + + // second backoff is 200ms, should retry 2nd time now + await advanceTimersByTime(50); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(3); + + // third backoff is 300ms, should not retry 3rd time yet + await advanceTimersByTime(280); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(3); + + // third backoff is 300ms, should retry 3rd time now + await advanceTimersByTime(20); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(4); + + const variation = await fetchPromise; + + expect(variation).toBe('var123'); + expect(mockMakeRequest.mock.calls.length).toBe(4); + for(let i = 0; i < 4; i++) { + assertRequest(i, mockMakeRequest, ruleId, userId, attributes, cmabUuid); + } + vi.useRealTimers(); + }); + + it('should reject the promise after retries are exhausted', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(Promise.reject('error'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + }, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow(); + expect(mockMakeRequest.mock.calls.length).toBe(6); + }); + + it('should reject the promise after retries are exhausted with error status', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(mockErrorResponse(500))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + }, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow(); + expect(mockMakeRequest.mock.calls.length).toBe(6); + }); + + it('should not retry if retryConfig is not provided', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValueOnce(getMockAbortableRequest(Promise.reject('error'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow(); + expect(mockMakeRequest.mock.calls.length).toBe(1); + }); + + it('should reject the promise if response status code is not 200', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(mockErrorResponse(500))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toMatchObject( + new OptimizelyError('CMAB_FETCH_FAILED', 500), + ); + }); + + it('should reject the promise if api response is not valid', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(Promise.resolve({ + statusCode: 200, + body: JSON.stringify({ + predictions: [], + }), + headers: {}, + }))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toMatchObject( + new OptimizelyError('INVALID_CMAB_RESPONSE'), + ); + }); + + it('should reject the promise if requestHandler.makeRequest rejects', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance<RequestHandler['makeRequest']> = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(Promise.reject('error'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const ruleId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow('error'); + }); +}); diff --git a/lib/core/decision_service/cmab/cmab_client.ts b/lib/core/decision_service/cmab/cmab_client.ts new file mode 100644 index 000000000..efe3a72ed --- /dev/null +++ b/lib/core/decision_service/cmab/cmab_client.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OptimizelyError } from "../../../error/optimizly_error"; +import { CMAB_FETCH_FAILED, INVALID_CMAB_FETCH_RESPONSE } from "../../../message/error_message"; +import { UserAttributes } from "../../../shared_types"; +import { runWithRetry } from "../../../utils/executor/backoff_retry_runner"; +import { sprintf } from "../../../utils/fns"; +import { RequestHandler } from "../../../utils/http_request_handler/http"; +import { isSuccessStatusCode } from "../../../utils/http_request_handler/http_util"; +import { BackoffController } from "../../../utils/repeater/repeater"; +import { Producer } from "../../../utils/type"; + +export interface CmabClient { + fetchDecision( + ruleId: string, + userId: string, + attributes: UserAttributes, + cmabUuid: string, + ): Promise<string> +} + +const CMAB_PREDICTION_ENDPOINT = 'https://prediction.cmab.optimizely.com/predict/%s'; + +export type RetryConfig = { + maxRetries: number, + backoffProvider?: Producer<BackoffController>; +} + +export type CmabClientConfig = { + requestHandler: RequestHandler, + retryConfig?: RetryConfig; +} + +export class DefaultCmabClient implements CmabClient { + private requestHandler: RequestHandler; + private retryConfig?: RetryConfig; + + constructor(config: CmabClientConfig) { + this.requestHandler = config.requestHandler; + this.retryConfig = config.retryConfig; + } + + async fetchDecision( + ruleId: string, + userId: string, + attributes: UserAttributes, + cmabUuid: string, + ): Promise<string> { + const url = sprintf(CMAB_PREDICTION_ENDPOINT, ruleId); + + const cmabAttributes = Object.keys(attributes).map((key) => ({ + id: key, + value: attributes[key], + type: 'custom_attribute', + })); + + const body = { + instances: [ + { + visitorId: userId, + experimentId: ruleId, + attributes: cmabAttributes, + cmabUUID: cmabUuid, + } + ] + } + + const variation = await (this.retryConfig ? + runWithRetry( + () => this.doFetch(url, JSON.stringify(body)), + this.retryConfig.backoffProvider?.(), + this.retryConfig.maxRetries, + ).result : this.doFetch(url, JSON.stringify(body)) + ); + + return variation; + } + + private async doFetch(url: string, data: string): Promise<string> { + const response = await this.requestHandler.makeRequest( + url, + { 'Content-Type': 'application/json' }, + 'POST', + data, + ).responsePromise; + + if (!isSuccessStatusCode(response.statusCode)) { + return Promise.reject(new OptimizelyError(CMAB_FETCH_FAILED, response.statusCode)); + } + + const body = JSON.parse(response.body); + if (!this.validateResponse(body)) { + return Promise.reject(new OptimizelyError(INVALID_CMAB_FETCH_RESPONSE)); + } + + return String(body.predictions[0].variation_id); + } + + private validateResponse(body: any): boolean { + return body.predictions && body.predictions.length > 0 && body.predictions[0].variation_id; + } +} diff --git a/lib/core/decision_service/cmab/cmab_service.spec.ts b/lib/core/decision_service/cmab/cmab_service.spec.ts new file mode 100644 index 000000000..38ee205e4 --- /dev/null +++ b/lib/core/decision_service/cmab/cmab_service.spec.ts @@ -0,0 +1,510 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi } from 'vitest'; + +import { DefaultCmabService } from './cmab_service'; +import { getMockSyncCache } from '../../../tests/mock/mock_cache'; +import { ProjectConfig } from '../../../project_config/project_config'; +import { OptimizelyDecideOption, UserAttributes } from '../../../shared_types'; +import OptimizelyUserContext from '../../../optimizely_user_context'; +import { validate as uuidValidate } from 'uuid'; +import { resolvablePromise } from '../../../utils/promise/resolvablePromise'; +import { exhaustMicrotasks } from '../../../tests/testUtils'; + +const mockProjectConfig = (): ProjectConfig => ({ + experimentIdMap: { + '1234': { + id: '1234', + key: 'cmab_1', + cmab: { + attributeIds: ['66', '77', '88'], + } + }, + '5678': { + id: '5678', + key: 'cmab_2', + cmab: { + attributeIds: ['66', '99'], + } + }, + }, + attributeIdMap: { + '66': { + id: '66', + key: 'country', + }, + '77': { + id: '77', + key: 'age', + }, + '88': { + id: '88', + key: 'language', + }, + '99': { + id: '99', + key: 'gender', + }, + } +} as any); + +const mockUserContext = (userId: string, attributes: UserAttributes): OptimizelyUserContext => new OptimizelyUserContext({ + userId, + attributes, +} as any); + +describe('DefaultCmabService', () => { + it('should fetch and return the variation from cmabClient using correct parameters', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValue('123'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext = mockUserContext('user123', { + country: 'US', + age: '25', + gender: 'male', + }); + + const ruleId = '1234'; + const variation = await cmabService.getDecision(projectConfig, userContext, ruleId, {}); + + expect(variation.variationId).toEqual('123'); + expect(uuidValidate(variation.cmabUuid)).toBe(true); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledOnce(); + const [ruleIdArg, userIdArg, attributesArg, cmabUuidArg] = mockCmabClient.fetchDecision.mock.calls[0]; + expect(ruleIdArg).toEqual(ruleId); + expect(userIdArg).toEqual(userContext.getUserId()); + expect(attributesArg).toEqual({ + country: 'US', + age: '25', + }); + }); + + it('should filter attributes based on experiment cmab attributeIds before fetching variation', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValue('123'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'male' + }); + + await cmabService.getDecision(projectConfig, userContext, '1234', {}); + await cmabService.getDecision(projectConfig, userContext, '5678', {}); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2); + expect(mockCmabClient.fetchDecision.mock.calls[0][2]).toEqual({ + country: 'US', + age: '25', + language: 'en', + }); + expect(mockCmabClient.fetchDecision.mock.calls[1][2]).toEqual({ + country: 'US', + gender: 'male' + }); + }); + + it('should cache the variation and return the same variation if relevant attributes have not changed', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456') + .mockResolvedValueOnce('789'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext11 = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'male' + }); + + const variation11 = await cmabService.getDecision(projectConfig, userContext11, '1234', {}); + + const userContext12 = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'female' + }); + + const variation12 = await cmabService.getDecision(projectConfig, userContext12, '1234', {}); + expect(variation11.variationId).toEqual('123'); + expect(variation12.variationId).toEqual('123'); + expect(variation11.cmabUuid).toEqual(variation12.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(1); + + const userContext21 = mockUserContext('user456', { + country: 'BD', + age: '30', + }); + + const variation21 = await cmabService.getDecision(projectConfig, userContext21, '5678', {}); + + const userContext22 = mockUserContext('user456', { + country: 'BD', + age: '35', + }); + + const variation22 = await cmabService.getDecision(projectConfig, userContext22, '5678', {}); + expect(variation21.variationId).toEqual('456'); + expect(variation22.variationId).toEqual('456'); + expect(variation21.cmabUuid).toEqual(variation22.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2); + }); + + it('should cache the variation and return the same variation if relevant attributes value have not changed but order changed', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456') + .mockResolvedValueOnce('789'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext11 = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'male' + }); + + const variation11 = await cmabService.getDecision(projectConfig, userContext11, '1234', {}); + + const userContext12 = mockUserContext('user123', { + gender: 'female', + language: 'en', + country: 'US', + age: '25', + }); + + const variation12 = await cmabService.getDecision(projectConfig, userContext12, '1234', {}); + expect(variation11.variationId).toEqual('123'); + expect(variation12.variationId).toEqual('123'); + expect(variation11.cmabUuid).toEqual(variation12.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(1); + }); + + it('should not mix up the cache between different experiments', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456') + .mockResolvedValueOnce('789'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext = mockUserContext('user123', { + country: 'US', + age: '25', + }); + + const variation1 = await cmabService.getDecision(projectConfig, userContext, '1234', {}); + + const variation2 = await cmabService.getDecision(projectConfig, userContext, '5678', {}); + + expect(variation1.variationId).toEqual('123'); + expect(variation2.variationId).toEqual('456'); + expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid); + }); + + it('should not mix up the cache between different users', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456') + .mockResolvedValueOnce('789'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + + const userContext1 = mockUserContext('user123', { + country: 'US', + age: '25', + }); + + const userContext2 = mockUserContext('user456', { + country: 'US', + age: '25', + }); + + const variation1 = await cmabService.getDecision(projectConfig, userContext1, '1234', {}); + + const variation2 = await cmabService.getDecision(projectConfig, userContext2, '1234', {}); + expect(variation1.variationId).toEqual('123'); + expect(variation2.variationId).toEqual('456'); + expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2); + }); + + it('should invalidate the cache and fetch a new variation if relevant attributes have changed', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext1 = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'male' + }); + + const variation1 = await cmabService.getDecision(projectConfig, userContext1, '1234', {}); + + const userContext2 = mockUserContext('user123', { + country: 'US', + age: '50', + language: 'en', + gender: 'male' + }); + + const variation2 = await cmabService.getDecision(projectConfig, userContext2, '1234', {}); + expect(variation1.variationId).toEqual('123'); + expect(variation2.variationId).toEqual('456'); + expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2); + }); + + it('should ignore the cache and fetch variation if IGNORE_CMAB_CACHE option is provided', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'male' + }); + + const variation1 = await cmabService.getDecision(projectConfig, userContext, '1234', {}); + + const variation2 = await cmabService.getDecision(projectConfig, userContext, '1234', { + [OptimizelyDecideOption.IGNORE_CMAB_CACHE]: true, + }); + + const variation3 = await cmabService.getDecision(projectConfig, userContext, '1234', {}); + + expect(variation1.variationId).toEqual('123'); + expect(variation2.variationId).toEqual('456'); + expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid); + + expect(variation3.variationId).toEqual('123'); + expect(variation3.cmabUuid).toEqual(variation1.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2); + }); + + it('should reset the cache before fetching variation if RESET_CMAB_CACHE option is provided', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456') + .mockResolvedValueOnce('789') + .mockResolvedValueOnce('101112'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext1 = mockUserContext('user123', { + country: 'US', + age: '25' + }); + + const userContext2 = mockUserContext('user456', { + country: 'US', + age: '50' + }); + + const variation1 = await cmabService.getDecision(projectConfig, userContext1, '1234', {}); + expect(variation1.variationId).toEqual('123'); + + const variation2 = await cmabService.getDecision(projectConfig, userContext2, '1234', {}); + expect(variation2.variationId).toEqual('456'); + + const variation3 = await cmabService.getDecision(projectConfig, userContext1, '1234', { + [OptimizelyDecideOption.RESET_CMAB_CACHE]: true, + }); + + expect(variation3.variationId).toEqual('789'); + + const variation4 = await cmabService.getDecision(projectConfig, userContext2, '1234', {}); + expect(variation4.variationId).toEqual('101112'); + }); + + it('should invalidate the cache and fetch a new variation if INVALIDATE_USER_CMAB_CACHE option is provided', async () => { + const mockCmabClient = { + fetchDecision: vi.fn().mockResolvedValueOnce('123') + .mockResolvedValueOnce('456'), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext = mockUserContext('user123', { + country: 'US', + age: '25', + language: 'en', + gender: 'male' + }); + + const variation1 = await cmabService.getDecision(projectConfig, userContext, '1234', {}); + + const variation2 = await cmabService.getDecision(projectConfig, userContext, '1234', { + [OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE]: true, + }); + + const variation3 = await cmabService.getDecision(projectConfig, userContext, '1234', {}); + + expect(variation1.variationId).toEqual('123'); + expect(variation2.variationId).toEqual('456'); + expect(variation1.cmabUuid).not.toEqual(variation2.cmabUuid); + expect(variation3.variationId).toEqual('456'); + expect(variation2.cmabUuid).toEqual(variation3.cmabUuid); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(2); + }); + + it('should serialize concurrent calls to getDecision with the same userId and ruleId', async () => { + const nCall = 10; + let currentVar = 123; + const fetchPromises = Array.from({ length: nCall }, () => resolvablePromise()); + + let callCount = 0; + const mockCmabClient = { + fetchDecision: vi.fn().mockImplementation(async () => { + const variation = `${currentVar++}`; + await fetchPromises[callCount++]; + return variation; + }), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext = mockUserContext('user123', {}); + + const resultPromises = []; + for (let i = 0; i < nCall; i++) { + resultPromises.push(cmabService.getDecision(projectConfig, userContext, '1234', {})); + } + + await exhaustMicrotasks(); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(1); + + for(let i = 0; i < nCall; i++) { + fetchPromises[i].resolve(''); + await exhaustMicrotasks(); + const result = await resultPromises[i]; + expect(result.variationId).toBe('123'); + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(1); + } + }); + + it('should not serialize calls to getDecision with different userId or ruleId', async () => { + let currentVar = 123; + const mockCmabClient = { + fetchDecision: vi.fn().mockImplementation(() => Promise.resolve(`${currentVar++}`)), + }; + + const cmabService = new DefaultCmabService({ + cmabCache: getMockSyncCache(), + cmabClient: mockCmabClient, + }); + + const projectConfig = mockProjectConfig(); + const userContext1 = mockUserContext('user123', {}); + const userContext2 = mockUserContext('user456', {}); + + const resultPromises = []; + resultPromises.push(cmabService.getDecision(projectConfig, userContext1, '1234', {})); + resultPromises.push(cmabService.getDecision(projectConfig, userContext1, '5678', {})); + resultPromises.push(cmabService.getDecision(projectConfig, userContext2, '1234', {})); + resultPromises.push(cmabService.getDecision(projectConfig, userContext2, '5678', {})); + + await exhaustMicrotasks(); + + expect(mockCmabClient.fetchDecision).toHaveBeenCalledTimes(4); + + for(let i = 0; i < resultPromises.length; i++) { + const result = await resultPromises[i]; + expect(result.variationId).toBe(`${123 + i}`); + } + }); +}); diff --git a/lib/core/decision_service/cmab/cmab_service.ts b/lib/core/decision_service/cmab/cmab_service.ts new file mode 100644 index 000000000..cd3ab99ea --- /dev/null +++ b/lib/core/decision_service/cmab/cmab_service.ts @@ -0,0 +1,180 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerFacade } from "../../../logging/logger"; +import { IOptimizelyUserContext } from "../../../optimizely_user_context"; +import { ProjectConfig } from "../../../project_config/project_config" +import { OptimizelyDecideOption, UserAttributes } from "../../../shared_types" +import { Cache, CacheWithRemove } from "../../../utils/cache/cache"; +import { CmabClient } from "./cmab_client"; +import { v4 as uuidV4 } from 'uuid'; +import murmurhash from "murmurhash"; +import { DecideOptionsMap } from ".."; +import { SerialRunner } from "../../../utils/executor/serial_runner"; + +export type CmabDecision = { + variationId: string, + cmabUuid: string, +} + +export interface CmabService { + /** + * Get variation id for the user + * @param {IOptimizelyUserContext} userContext + * @param {string} ruleId + * @param {OptimizelyDecideOption[]} options + * @return {Promise<CmabDecision>} + */ + getDecision( + projectConfig: ProjectConfig, + userContext: IOptimizelyUserContext, + ruleId: string, + options: DecideOptionsMap, + ): Promise<CmabDecision> +} + +export type CmabCacheValue = { + attributesHash: string, + variationId: string, + cmabUuid: string, +} + +export type CmabServiceOptions = { + logger?: LoggerFacade; + cmabCache: CacheWithRemove<CmabCacheValue>; + cmabClient: CmabClient; +} + +const SERIALIZER_BUCKETS = 1000; + +export class DefaultCmabService implements CmabService { + private cmabCache: CacheWithRemove<CmabCacheValue>; + private cmabClient: CmabClient; + private logger?: LoggerFacade; + private serializers: SerialRunner[] = Array.from( + { length: SERIALIZER_BUCKETS }, () => new SerialRunner() + ); + + constructor(options: CmabServiceOptions) { + this.cmabCache = options.cmabCache; + this.cmabClient = options.cmabClient; + this.logger = options.logger; + } + + private getSerializerIndex(userId: string, experimentId: string): number { + const key = this.getCacheKey(userId, experimentId); + const hash = murmurhash.v3(key); + return Math.abs(hash) % SERIALIZER_BUCKETS; + } + + async getDecision( + projectConfig: ProjectConfig, + userContext: IOptimizelyUserContext, + ruleId: string, + options: DecideOptionsMap, + ): Promise<CmabDecision> { + const serializerIndex = this.getSerializerIndex(userContext.getUserId(), ruleId); + return this.serializers[serializerIndex].run(() => + this.getDecisionInternal(projectConfig, userContext, ruleId, options) + ); + } + + private async getDecisionInternal( + projectConfig: ProjectConfig, + userContext: IOptimizelyUserContext, + ruleId: string, + options: DecideOptionsMap, + ): Promise<CmabDecision> { + const filteredAttributes = this.filterAttributes(projectConfig, userContext, ruleId); + + if (options[OptimizelyDecideOption.IGNORE_CMAB_CACHE]) { + return this.fetchDecision(ruleId, userContext.getUserId(), filteredAttributes); + } + + if (options[OptimizelyDecideOption.RESET_CMAB_CACHE]) { + this.cmabCache.reset(); + } + + const cacheKey = this.getCacheKey(userContext.getUserId(), ruleId); + + if (options[OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE]) { + this.cmabCache.remove(cacheKey); + } + + const cachedValue = await this.cmabCache.lookup(cacheKey); + + const attributesJson = JSON.stringify(filteredAttributes, Object.keys(filteredAttributes).sort()); + const attributesHash = String(murmurhash.v3(attributesJson)); + + if (cachedValue) { + if (cachedValue.attributesHash === attributesHash) { + return { variationId: cachedValue.variationId, cmabUuid: cachedValue.cmabUuid }; + } else { + this.cmabCache.remove(cacheKey); + } + } + + const variation = await this.fetchDecision(ruleId, userContext.getUserId(), filteredAttributes); + this.cmabCache.save(cacheKey, { + attributesHash, + variationId: variation.variationId, + cmabUuid: variation.cmabUuid, + }); + + return variation; + } + + private async fetchDecision( + ruleId: string, + userId: string, + attributes: UserAttributes, + ): Promise<CmabDecision> { + const cmabUuid = uuidV4(); + const variationId = await this.cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); + return { variationId, cmabUuid }; + } + + private filterAttributes( + projectConfig: ProjectConfig, + userContext: IOptimizelyUserContext, + ruleId: string + ): UserAttributes { + const filteredAttributes: UserAttributes = {}; + const userAttributes = userContext.getAttributes(); + + const experiment = projectConfig.experimentIdMap[ruleId]; + if (!experiment || !experiment.cmab) { + return filteredAttributes; + } + + const cmabAttributeIds = experiment.cmab.attributeIds; + + cmabAttributeIds.forEach((aid) => { + const attribute = projectConfig.attributeIdMap[aid]; + + if (userAttributes.hasOwnProperty(attribute.key)) { + filteredAttributes[attribute.key] = userAttributes[attribute.key]; + } + }); + + return filteredAttributes; + } + + private getCacheKey(userId: string, ruleId: string): string { + const len = userId.length; + return `${len}-${userId}-${ruleId}`; + } +} diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts new file mode 100644 index 000000000..9fc9d89a1 --- /dev/null +++ b/lib/core/decision_service/index.spec.ts @@ -0,0 +1,2939 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi, MockInstance, beforeEach, afterEach } from 'vitest'; +import { CMAB_DUMMY_ENTITY_ID, CMAB_FETCH_FAILED, DecisionService } from '.'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import OptimizelyUserContext from '../../optimizely_user_context'; +import { bucket } from '../bucketer'; +import { getTestProjectConfig, getTestProjectConfigWithFeatures } from '../../tests/test_data'; +import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; +import { BucketerParams, Experiment, Holdout, OptimizelyDecideOption, UserAttributes, UserProfile } from '../../shared_types'; +import { CONTROL_ATTRIBUTES, DECISION_SOURCES } from '../../utils/enums'; +import { getDecisionTestDatafile } from '../../tests/decision_test_datafile'; +import { Value } from '../../utils/promise/operation_value'; +import { + USER_HAS_NO_FORCED_VARIATION, + VALID_BUCKETING_ID, + SAVED_USER_VARIATION, + SAVED_VARIATION_NOT_FOUND, +} from 'log_message'; +import { + EXPERIMENT_NOT_RUNNING, + RETURNING_STORED_VARIATION, + USER_NOT_IN_EXPERIMENT, + USER_FORCED_IN_VARIATION, + EVALUATING_AUDIENCES_COMBINED, + AUDIENCE_EVALUATION_RESULT_COMBINED, + USER_IN_ROLLOUT, + USER_NOT_IN_ROLLOUT, + FEATURE_HAS_NO_EXPERIMENTS, + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + USER_BUCKETED_INTO_TARGETING_RULE, + NO_ROLLOUT_EXISTS, + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, +} from '../decision_service/index'; +import { BUCKETING_ID_NOT_STRING, USER_PROFILE_LOOKUP_ERROR, USER_PROFILE_SAVE_ERROR } from 'error_message'; + +type MockLogger = ReturnType<typeof getMockLogger>; + +type MockFnType = ReturnType<typeof vi.fn>; + +type MockUserProfileService = { + lookup: MockFnType; + save: MockFnType; +}; + +type MockCmabService = { + getDecision: MockFnType; +} + +type DecisionServiceInstanceOpt = { + logger?: boolean; + userProfileService?: boolean; + userProfileServiceAsync?: boolean; +} + +type DecisionServiceInstance = { + logger?: MockLogger; + userProfileService?: MockUserProfileService; + userProfileServiceAsync?: MockUserProfileService; + cmabService: MockCmabService; + decisionService: DecisionService; +} + +const getDecisionService = (opt: DecisionServiceInstanceOpt = {}): DecisionServiceInstance => { + const logger = opt.logger ? getMockLogger() : undefined; + const userProfileService = opt.userProfileService ? { + lookup: vi.fn(), + save: vi.fn(), + } : undefined; + + const userProfileServiceAsync = opt.userProfileServiceAsync ? { + lookup: vi.fn(), + save: vi.fn(), + } : undefined; + + const cmabService = { + getDecision: vi.fn(), + }; + + const decisionService = new DecisionService({ + logger, + userProfileService, + userProfileServiceAsync, + UNSTABLE_conditionEvaluators: {}, + cmabService, + }); + + return { + logger, + userProfileService, + userProfileServiceAsync, + decisionService, + cmabService, + }; +}; + +const mockBucket: MockInstance<typeof bucket> = vi.hoisted(() => vi.fn()); + +vi.mock('../bucketer', () => ({ + bucket: mockBucket, +})); + +// Mock the feature toggle for holdout tests +const mockHoldoutToggle = vi.hoisted(() => vi.fn()); + +vi.mock('../../feature_toggle', () => ({ + holdout: mockHoldoutToggle, +})); + + +const cloneDeep = (d: any) => JSON.parse(JSON.stringify(d)); + +const testData = getTestProjectConfig(); +const testDataWithFeatures = getTestProjectConfigWithFeatures(); + +// Utility function to create test datafile with holdout configurations +const getHoldoutTestDatafile = () => { + const datafile = getDecisionTestDatafile(); + + // Add holdouts to the datafile + datafile.holdouts = [ + { + id: 'holdout_running_id', + key: 'holdout_running', + status: 'Running', + includedFlags: [], + excludedFlags: [], + audienceIds: ['4001'], // age_22 audience + audienceConditions: ['or', '4001'], + variations: [ + { + id: 'holdout_variation_running_id', + key: 'holdout_variation_running', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'holdout_variation_running_id', + endOfRange: 5000 + } + ] + }, + { + id: "holdout_not_bucketed_id", + key: "holdout_not_bucketed", + status: "Running", + includedFlags: [], + excludedFlags: [], + audienceIds: ['4002'], + audienceConditions: ['or', '4002'], + variations: [ + { + id: 'holdout_not_bucketed_variation_id', + key: 'holdout_not_bucketed_variation', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'holdout_not_bucketed_variation_id', + endOfRange: 0, + } + ] + }, + ]; + + return datafile; +}; + +const verifyBucketCall = ( + call: number, + projectConfig: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext, + bucketIdFromAttribute = false, +) => { + const { + experimentId, + experimentKey, + userId, + trafficAllocationConfig, + experimentKeyMap, + experimentIdMap, + groupIdMap, + variationIdMap, + bucketingId, + } = mockBucket.mock.calls[call][0]; + let expectedTrafficAllocation = experiment.trafficAllocation; + if (experiment.cmab) { + expectedTrafficAllocation = [{ + endOfRange: experiment.cmab.trafficAllocation, + entityId: CMAB_DUMMY_ENTITY_ID, + }]; + } + + expect(experimentId).toBe(experiment.id); + expect(experimentKey).toBe(experiment.key); + expect(userId).toBe(user.getUserId()); + expect(trafficAllocationConfig).toEqual(expectedTrafficAllocation); + expect(experimentKeyMap).toBe(projectConfig.experimentKeyMap); + expect(experimentIdMap).toBe(projectConfig.experimentIdMap); + expect(groupIdMap).toBe(projectConfig.groupIdMap); + expect(variationIdMap).toBe(projectConfig.variationIdMap); + expect(bucketingId).toBe(bucketIdFromAttribute ? user.getAttributes()[CONTROL_ATTRIBUTES.BUCKETING_ID] : user.getUserId()); +}; + +describe('DecisionService', () => { + describe('getVariation', function() { + beforeEach(() => { + mockBucket.mockClear(); + }); + + it('should return the correct variation from bucketer for the given experiment key and user ID for a running experiment', () => { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + + const experiment = config.experimentIdMap['111127']; + + mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + + const { decisionService } = getDecisionService(); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('control'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + }); + + it('should use $opt_bucketing_id attribute as bucketing id if provided', () => { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + $opt_bucketing_id: 'test_bucketing_id', + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + + const experiment = config.experimentIdMap['111127']; + + mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + + const { decisionService } = getDecisionService(); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('control'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user, true); + }); + + it('should return the whitelisted variation if the user is whitelisted', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user2' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService, logger } = getDecisionService({ logger: true }); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('variationWithAudience'); + expect(mockBucket).not.toHaveBeenCalled(); + expect(logger?.debug).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenCalledTimes(1); + + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user2'); + expect(logger?.info).toHaveBeenNthCalledWith(1, USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience'); + }); + + it('should return null if the user does not meet audience conditions', () => { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user3' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService, logger } = getDecisionService({ logger: true }); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe(null); + expect(mockBucket).not.toHaveBeenCalled(); + + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'user3'); + expect(logger?.debug).toHaveBeenNthCalledWith(2, EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])); + + expect(logger?.info).toHaveBeenNthCalledWith(1, AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE'); + expect(logger?.info).toHaveBeenNthCalledWith(2, USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences'); + }); + + it('should return the forced variation set using setForcedVariation \ + in presence of a whitelisted variation', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user2' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService } = getDecisionService(); + + const forcedVariation = 'controlWithAudience'; + + decisionService.setForcedVariation(config, experiment.key, user.getUserId(), forcedVariation); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe(forcedVariation); + + const whitelistedVariation = experiment.forcedVariations?.[user.getUserId()]; + expect(whitelistedVariation).toBeDefined(); + expect(whitelistedVariation).not.toEqual(forcedVariation); + }); + + it('should return the forced variation set using setForcedVariation \ + even if user does not satisfy audience condition', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user3', // no attributes are set, should not satisfy audience condition 11154 + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['122227']; + + const { decisionService } = getDecisionService(); + + const forcedVariation = 'controlWithAudience'; + + decisionService.setForcedVariation(config, experiment.key, user.getUserId(), forcedVariation); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe(forcedVariation); + }); + + it('should return null if the experiment is not running', function() { + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'user1' + }); + + const config = createProjectConfig(cloneDeep(testData)); + + const experiment = config.experimentIdMap['133337']; + + const { decisionService, logger } = getDecisionService({ logger: true }); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe(null); + expect(mockBucket).not.toHaveBeenCalled(); + expect(logger?.info).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenNthCalledWith(1, EXPERIMENT_NOT_RUNNING, 'testExperimentNotRunning'); + }); + + it('should respect the sticky bucketing information for attributes when attributes.$opt_experiment_bucket_map is supplied', () => { + const fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: UserAttributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + mockBucket.mockReturnValue(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + + const { decisionService } = getDecisionService(); + + const variation = decisionService.getVariation(config, experiment, user); + + expect(variation.result).toBe('variation'); + expect(mockBucket).not.toHaveBeenCalled(); + }); + + describe('when a user profile service is provided', function() { + beforeEach(() => { + mockBucket.mockClear(); + }); + + it('should return the previously bucketed variation', () => { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }); + + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + expect(mockBucket).not.toHaveBeenCalled(); + + expect(logger?.debug).toHaveBeenCalledTimes(1); + expect(logger?.info).toHaveBeenCalledTimes(1); + + expect(logger?.debug).toHaveBeenNthCalledWith(1, USER_HAS_NO_FORCED_VARIATION, 'decision_service_user'); + expect(logger?.info).toHaveBeenNthCalledWith(1, RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user'); + }); + + it('should bucket and save user profile if there was no prevously bucketed variation', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: {}, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should bucket if the user profile service returns null', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue(null); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should re-bucket if the stored variation is no longer valid', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: 'not valid variation', + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(logger?.debug).toHaveBeenCalledWith(USER_HAS_NO_FORCED_VARIATION, 'decision_service_user'); + expect(logger?.info).toHaveBeenCalledWith(SAVED_VARIATION_NOT_FOUND, 'decision_service_user', 'not valid variation', 'testExperiment'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should store the bucketed variation for the user', function() { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: {}, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + + it('should log an error message and bucket if "lookup" throws an error', () => { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockImplementation(() => { + throw new Error('I am an error'); + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(logger?.debug).toHaveBeenCalledWith(USER_HAS_NO_FORCED_VARIATION, 'decision_service_user'); + expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_LOOKUP_ERROR, 'decision_service_user', 'I am an error'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should log an error message if "save" throws an error', () => { + mockBucket.mockReturnValue({ + result: '111128', // ID of the 'control' variation + reasons: [], + }); + + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue(null); + userProfileService?.save.mockImplementation(() => { + throw new Error('I am an error'); + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('decision_service_user'); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, experiment, user); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + + expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_SAVE_ERROR, 'decision_service_user', 'I am an error'); + }); + + it('should respect $opt_experiment_bucket_map attribute over the userProfileService for the matching experiment id', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: UserAttributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + }); + + it('should ignore attributes for a different experiment id', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: UserAttributes = { + $opt_experiment_bucket_map: { + '122227': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('control'); + }); + + it('should use $ opt_experiment_bucket_map attribute when the userProfile contains variations for other experiments', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '122227': { + variation_id: '122229', // ID of the 'variationWithAudience' variation + }, + }, + }); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: UserAttributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + }); + + it('should use attributes when the userProfileLookup returns null', function() { + const { decisionService, userProfileService, logger } = getDecisionService({ userProfileService: true, logger: true }); + + userProfileService?.lookup.mockReturnValue(null); + + const config = createProjectConfig(cloneDeep(testData)); + const experiment = config.experimentIdMap['111127']; + + const attributes: UserAttributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'decision_service_user', + attributes, + }); + + const variation = decisionService.getVariation(config, experiment, user); + expect(variation.result).toBe('variation'); + }); + }); + }); + + describe('getVariationForFeature - sync', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should return variation from the first experiment for which a variation is available', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + op, + config, + experiment: any, + user, + decideOptions, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + return Value.of('sync', { + result: { variationKey: 'variation_2' }, + reasons: [], + }); + } + return Value.of('sync', { + result: {}, + reasons: [], + }); + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(2); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + 'sync', config, config.experimentKeyMap['exp_1'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + 'sync', config, config.experimentKeyMap['exp_2'], user, expect.anything(), expect.anything()); + }); + + it('should return the variation forced for an experiment in the userContext if available', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + op, + config, + experiment: any, + user, + decideOptions, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + return Value.of('sync', { + result: { varationKey: 'variation_2' }, + reasons: [], + }); + } + return Value.of('sync', { + result: {}, + reasons: [], + }); + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + user.setForcedDecision( + { flagKey: 'flag_1', ruleKey: 'exp_2' }, + { variationKey: 'variation_5' } + ); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should save the variation found for an experiment in the user profile', () => { + const { decisionService, userProfileService } = getDecisionService({ userProfileService: true }); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + op, + config, + experiment: any, + user, + decideOptions, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + const variation = 'variation_2'; + + userProfileTracker.userProfile[experiment.id] = { + variation_id: '5002', + }; + userProfileTracker.isProfileUpdated = true; + + return Value.of('sync', { + result: { variationKey: 'variation_2' }, + reasons: [], + }); + } + return Value.of('sync', { + result: {}, + reasons: [], + }); + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + userProfileService?.lookup.mockImplementation((userId: string) => { + if (userId === 'tester') { + return { + user_id: 'tester', + experiment_bucket_map: { + '2001': { + variation_id: '5001', + }, + }, + }; + } + return null; + }); + + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('tester'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'tester', + experiment_bucket_map: { + '2001': { + variation_id: '5001', + }, + '2002': { + variation_id: '5002', + }, + }, + }); + }); + + describe('when no variation is found for any experiment and a targeted delivery \ + audience condition is satisfied', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should return variation from the target delivery for which audience condition \ + is satisfied if the user is bucketed into it', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue(Value.of('sync', { + result: {}, + reasons: [], + })); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 and delivery_3 + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'delivery_2') { + return { + result: '5005', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_2'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + 'sync', config, config.experimentKeyMap['exp_1'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + 'sync', config, config.experimentKeyMap['exp_2'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + 'sync', config, config.experimentKeyMap['exp_3'], user, expect.anything(), expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, config.experimentIdMap['3002'], user); + }); + + it('should return variation from the target delivery and use $opt_bucketing_id attribute as bucketing id', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue(Value.of('sync', { + result: {}, + reasons: [], + })); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 and delivery_3 + $opt_bucketing_id: 'test_bucketing_id', + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'delivery_2') { + return { + result: '5005', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_2'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + 'sync', config, config.experimentKeyMap['exp_1'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + 'sync', config, config.experimentKeyMap['exp_2'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + 'sync', config, config.experimentKeyMap['exp_3'], user, expect.anything(), expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, config.experimentIdMap['3002'], user, true); + }); + + it('should skip to everyone else targeting rule if the user is not bucketed \ + into the targeted delivery for which audience condition is satisfied', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue(Value.of('sync', { + result: {}, + reasons: [], + })); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // this should satisfy the audience condition for the targeted delivery with key delivery_2 and delivery_3 + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'default-rollout-key') { + return { + result: '5007', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentIdMap['default-rollout-id'], + variation: config.variationIdMap['5007'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + 'sync', config, config.experimentKeyMap['exp_1'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + 'sync', config, config.experimentKeyMap['exp_2'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + 'sync', config, config.experimentKeyMap['exp_3'], user, expect.anything(), expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(2); + verifyBucketCall(0, config, config.experimentIdMap['3002'], user); + verifyBucketCall(1, config, config.experimentIdMap['default-rollout-id'], user); + }); + }); + + it('should return the forced variation for targeted delivery rule when no variation \ + is found for any experiment and a there is a forced decision \ + for a targeted delivery in the userContext', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + op, + config, + experiment: any, + user, + decideOptions, + userProfileTracker: any, + ) => { + return Value.of('sync', { + result: {}, + reasons: [], + }); + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + }); + + user.setForcedDecision( + { flagKey: 'flag_1', ruleKey: 'delivery_2' }, + { variationKey: 'variation_1' } + ); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_2'], + variation: config.variationIdMap['5001'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + }); + + it('should return variation from the everyone else targeting rule if no variation \ + is found for any experiment or targeted delivery', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue(Value.of('sync', { + result: {}, + reasons: [], + })); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 100, // this should not satisfy any audience condition for any targeted delivery + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'default-rollout-key') { + return { + result: '5007', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: config.experimentIdMap['default-rollout-id'], + variation: config.variationIdMap['5007'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + 'sync', config, config.experimentKeyMap['exp_1'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + 'sync', config, config.experimentKeyMap['exp_2'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + 'sync', config, config.experimentKeyMap['exp_3'], user, expect.anything(), expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(1); + verifyBucketCall(0, config, config.experimentIdMap['default-rollout-id'], user); + }); + + it('should return null if no variation is found for any experiment, targeted delivery, or everyone else targeting rule', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockReturnValue(Value.of('sync', { + result: {}, + reasons: [], + })); + + const config = createProjectConfig(getDecisionTestDatafile()); + const rolloutId = config.featureKeyMap['flag_1'].rolloutId; + config.rolloutIdMap[rolloutId].experiments = []; // remove the experiments from the rollout + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 10, + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const variation = decisionService.getVariationForFeature(config, feature, user); + + expect(variation.result).toEqual({ + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(resolveVariationSpy).toHaveBeenCalledTimes(3); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(1, + 'sync', config, config.experimentKeyMap['exp_1'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(2, + 'sync', config, config.experimentKeyMap['exp_2'], user, expect.anything(), expect.anything()); + expect(resolveVariationSpy).toHaveBeenNthCalledWith(3, + 'sync', config, config.experimentKeyMap['exp_3'], user, expect.anything(), expect.anything()); + + expect(mockBucket).toHaveBeenCalledTimes(0); + }); + }); + + describe('resolveVariationForFeatureList - async', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should return variation from the first experiment for which a variation is available', async () => { + const { decisionService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 15, // should satisfy audience condition for all experiments + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'exp_2') { + return { + result: '5002', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should not return variation and should not call cmab service \ + for cmab experiment if user is not bucketed into it', async () => { + const { decisionService, cmabService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'default-rollout-key') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['default-rollout-key'], + variation: config.variationIdMap['5007'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + verifyBucketCall(0, config, config.experimentKeyMap['exp_3'], user); + expect(cmabService.getDecision).not.toHaveBeenCalled(); + }); + + it('should get decision from the cmab service if the experiment is a cmab experiment \ + and user is bucketed into it', async () => { + const { decisionService, cmabService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockResolvedValue({ + variationId: '5003', + cmabUuid: 'uuid-test', + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + expect(variation.result).toEqual({ + cmabUuid: 'uuid-test', + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5003'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + verifyBucketCall(0, config, config.experimentKeyMap['exp_3'], user); + + expect(cmabService.getDecision).toHaveBeenCalledTimes(1); + expect(cmabService.getDecision).toHaveBeenCalledWith( + config, + user, + '2003', // id of exp_3 + {}, + ); + }); + + it('should pass the correct DecideOptionMap to cmabService', async () => { + const { decisionService, cmabService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockResolvedValue({ + variationId: '5003', + cmabUuid: 'uuid-test', + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, { + [OptimizelyDecideOption.RESET_CMAB_CACHE]: true, + [OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE]: true, + }).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + expect(variation.result).toEqual({ + cmabUuid: 'uuid-test', + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5003'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(cmabService.getDecision).toHaveBeenCalledTimes(1); + expect(cmabService.getDecision).toHaveBeenCalledWith( + config, + user, + '2003', // id of exp_3 + { + [OptimizelyDecideOption.RESET_CMAB_CACHE]: true, + [OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE]: true, + }, + ); + }); + + it('should return error if cmab getDecision fails', async () => { + const { decisionService, cmabService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockRejectedValue(new Error('I am an error')); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.error).toBe(true); + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_3'], + variation: null, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variation.reasons).toContainEqual( + [CMAB_FETCH_FAILED, 'exp_3'], + ); + + expect(cmabService.getDecision).toHaveBeenCalledTimes(1); + expect(cmabService.getDecision).toHaveBeenCalledWith( + config, + user, + '2003', // id of exp_3 + {}, + ); + }); + + it('should use userProfileServiceAsync if available and sync user profile service is unavialable', async () => { + const { decisionService, cmabService, userProfileServiceAsync } = getDecisionService({ + userProfileService: false, + userProfileServiceAsync: true, + }); + + userProfileServiceAsync?.lookup.mockImplementation((userId: string) => { + if (userId === 'tester-1') { + return Promise.resolve({ + user_id: 'tester-1', + experiment_bucket_map: { + '2003': { + variation_id: '5001', + }, + }, + }); + } + return Promise.resolve(null); + }); + + userProfileServiceAsync?.save.mockImplementation(() => Promise.resolve()); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockResolvedValue({ + variationId: '5003', + cmabUuid: 'uuid-test', + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user1 = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester-1', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const user2 = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester-2', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user1, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5001'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(cmabService.getDecision).not.toHaveBeenCalled(); + expect(userProfileServiceAsync?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileServiceAsync?.lookup).toHaveBeenCalledWith('tester-1'); + + const value2 = decisionService.resolveVariationsForFeatureList('async', config, [feature], user2, {}).get(); + expect(value2).toBeInstanceOf(Promise); + + const variation2 = (await value2)[0]; + expect(variation2.result).toEqual({ + cmabUuid: 'uuid-test', + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5003'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileServiceAsync?.lookup).toHaveBeenCalledTimes(2); + expect(userProfileServiceAsync?.lookup).toHaveBeenNthCalledWith(2, 'tester-2'); + expect(userProfileServiceAsync?.save).toHaveBeenCalledTimes(1); + expect(userProfileServiceAsync?.save).toHaveBeenCalledWith({ + user_id: 'tester-2', + experiment_bucket_map: { + '2003': { + variation_id: '5003', + }, + }, + }); + }); + + it('should log error and perform normal decision fetch if async userProfile lookup fails', async () => { + const { decisionService, cmabService, userProfileServiceAsync, logger } = getDecisionService({ + userProfileService: false, + userProfileServiceAsync: true, + logger: true, + }); + + userProfileServiceAsync?.lookup.mockImplementation((userId: string) => { + return Promise.reject(new Error('I am an error')); + }); + + userProfileServiceAsync?.save.mockImplementation(() => Promise.resolve()); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockResolvedValue({ + variationId: '5003', + cmabUuid: 'uuid-test', + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + cmabUuid: 'uuid-test', + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5003'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileServiceAsync?.lookup).toHaveBeenCalledWith('tester'); + expect(cmabService.getDecision).toHaveBeenCalledTimes(1); + expect(cmabService.getDecision).toHaveBeenCalledWith( + config, + user, + '2003', // id of exp_3 + {}, + ); + + expect(logger?.error).toHaveBeenCalledTimes(1); + expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_LOOKUP_ERROR, 'tester', 'I am an error'); + }); + + it('should log error async userProfile save fails', async () => { + const { decisionService, cmabService, userProfileServiceAsync, logger } = getDecisionService({ + userProfileService: false, + userProfileServiceAsync: true, + logger: true, + }); + + userProfileServiceAsync?.lookup.mockResolvedValue(null); + + userProfileServiceAsync?.save.mockRejectedValue(new Error('I am an error')); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockResolvedValue({ + variationId: '5003', + cmabUuid: 'uuid-test', + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + cmabUuid: 'uuid-test', + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5003'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileServiceAsync?.lookup).toHaveBeenCalledWith('tester'); + expect(cmabService.getDecision).toHaveBeenCalledTimes(1); + expect(cmabService.getDecision).toHaveBeenCalledWith( + config, + user, + '2003', // id of exp_3 + {}, + ); + + expect(userProfileServiceAsync?.save).toHaveBeenCalledTimes(1); + expect(userProfileServiceAsync?.save).toHaveBeenCalledWith({ + user_id: 'tester', + experiment_bucket_map: { + '2003': { + variation_id: '5003', + }, + }, + }); + expect(logger?.error).toHaveBeenCalledTimes(1); + expect(logger?.error).toHaveBeenCalledWith(USER_PROFILE_SAVE_ERROR, 'tester', 'I am an error'); + }); + + it('should use the sync user profile service if both sync and async ups are provided', async () => { + const { decisionService, userProfileService, userProfileServiceAsync, cmabService } = getDecisionService({ + userProfileService: true, + userProfileServiceAsync: true, + }); + + userProfileService?.lookup.mockReturnValue(null); + userProfileService?.save.mockReturnValue(null); + + userProfileServiceAsync?.lookup.mockResolvedValue(null); + userProfileServiceAsync?.save.mockResolvedValue(null); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + cmabService.getDecision.mockResolvedValue({ + variationId: '5003', + cmabUuid: 'uuid-test', + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + cmabUuid: 'uuid-test', + experiment: config.experimentKeyMap['exp_3'], + variation: config.variationIdMap['5003'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('tester'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'tester', + experiment_bucket_map: { + '2003': { + variation_id: '5003', + }, + }, + }); + + expect(userProfileServiceAsync?.lookup).not.toHaveBeenCalled(); + expect(userProfileServiceAsync?.save).not.toHaveBeenCalled(); + }); + + describe('holdout', () => { + beforeEach(async() => { + mockHoldoutToggle.mockReturnValue(true); + const actualBucketModule = (await vi.importActual('../bucketer')) as { bucket: typeof bucket }; + mockBucket.mockImplementation(actualBucketModule.bucket); + }); + + it('should return holdout variation when user is bucketed into running holdout', async () => { + const { decisionService } = getDecisionService(); + const config = createProjectConfig(getHoldoutTestDatafile()); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 20, + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.holdoutIdMap && config.holdoutIdMap['holdout_running_id'], + variation: config.variationIdMap['holdout_variation_running_id'], + decisionSource: DECISION_SOURCES.HOLDOUT, + }); + }); + + it("should consider global holdout even if local holdout is present", async () => { + const { decisionService } = getDecisionService(); + const datafile = getHoldoutTestDatafile(); + const newEntry = { + id: 'holdout_included_id', + key: 'holdout_included', + status: 'Running', + includedFlags: ['1001'], + excludedFlags: [], + audienceIds: ['4002'], // age_40 audience + audienceConditions: ['or', '4002'], + variations: [ + { + id: 'holdout_variation_included_id', + key: 'holdout_variation_included', + variables: [], + }, + ], + trafficAllocation: [ + { + entityId: 'holdout_variation_included_id', + endOfRange: 5000, + }, + ], + }; + datafile.holdouts = [newEntry, ...datafile.holdouts]; + const config = createProjectConfig(datafile); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 20, // satisfies both global holdout (age_22) and included holdout (age_40) audiences + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.holdoutIdMap && config.holdoutIdMap['holdout_running_id'], + variation: config.variationIdMap['holdout_variation_running_id'], + decisionSource: DECISION_SOURCES.HOLDOUT, + }); + }); + + it("should consider local holdout if misses global holdout", async () => { + const { decisionService } = getDecisionService(); + const datafile = getHoldoutTestDatafile(); + + datafile.holdouts.push({ + id: 'holdout_included_specific_id', + key: 'holdout_included_specific', + status: 'Running', + includedFlags: ['1001'], + excludedFlags: [], + audienceIds: ['4002'], // age_60 audience (age <= 60) + audienceConditions: ['or', '4002'], + variations: [ + { + id: 'holdout_variation_included_specific_id', + key: 'holdout_variation_included_specific', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'holdout_variation_included_specific_id', + endOfRange: 5000 + } + ] + }); + const config = createProjectConfig(datafile); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'test_holdout_user', + attributes: { + age: 50, // Does not satisfy global holdout (age_22, age <= 22) but satisfies included holdout (age_60, age <= 60) + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.holdoutIdMap && config.holdoutIdMap['holdout_included_specific_id'], + variation: config.variationIdMap['holdout_variation_included_specific_id'], + decisionSource: DECISION_SOURCES.HOLDOUT, + }); + }); + + it('should fallback to experiment when holdout status is not running', async () => { + const { decisionService } = getDecisionService(); + const datafile = getHoldoutTestDatafile(); + + datafile.holdouts = datafile.holdouts.map((holdout: Holdout) => { + if(holdout.id === 'holdout_running_id') { + return { + ...holdout, + status: "Draft" + } + } + return holdout; + }); + + const config = createProjectConfig(datafile); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 15, + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_1'], + variation: config.variationIdMap['5001'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should fallback to experiment when user does not meet holdout audience conditions', async () => { + const { decisionService } = getDecisionService(); + const config = createProjectConfig(getHoldoutTestDatafile()); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 30, // does not satisfy age_22 audience condition for holdout_running + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should fallback to experiment when user is not bucketed into holdout traffic', async () => { + const { decisionService } = getDecisionService(); + const config = createProjectConfig(getHoldoutTestDatafile()); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 50, + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should fallback to rollout when no holdout or experiment matches', async () => { + const { decisionService } = getDecisionService(); + const datafile = getHoldoutTestDatafile(); + // Modify the datafile to create proper audience conditions for this test + // Make exp_1 and exp_2 use age conditions that won't match our test user + datafile.audiences = datafile.audiences.map((audience: any) => { + if (audience.id === '4001') { // age_22 + return { + ...audience, + conditions: JSON.stringify(["or", {"match": "exact", "name": "age", "type": "custom_attribute", "value": 22}]) + }; + } + if (audience.id === '4002') { // age_60 + return { + ...audience, + conditions: JSON.stringify(["or", {"match": "exact", "name": "age", "type": "custom_attribute", "value": 60}]) + }; + } + return audience; + }); + + // Make exp_2 use a different audience so it won't conflict with delivery_2 + datafile.experiments = datafile.experiments.map((experiment: any) => { + if (experiment.key === 'exp_2') { + return { + ...experiment, + audienceIds: ['4001'], // Change from 4002 to 4001 (age_22) + audienceConditions: ['or', '4001'] + }; + } + return experiment; + }); + + const config = createProjectConfig(datafile); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 60, // matches audience 4002 (age_60) used by delivery_2, but not experiments + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_2'], + variation: config.variationIdMap['5005'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + }); + + it('should skip holdouts excluded for specific flags', async () => { + const { decisionService } = getDecisionService(); + const datafile = getHoldoutTestDatafile(); + + datafile.holdouts = datafile.holdouts.map((holdout: any) => { + if(holdout.id === 'holdout_running_id') { + return { + ...holdout, + excludedFlags: ['1001'] + } + } + return holdout; + }); + + const config = createProjectConfig(datafile); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 15, // satisfies age_22 audience condition (age <= 22) for global holdout, but holdout excludes flag_1 + }, + }); + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_1'], + variation: config.variationIdMap['5001'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should handle multiple holdouts and use first matching one', async () => { + const { decisionService } = getDecisionService(); + const datafile = getHoldoutTestDatafile(); + + datafile.holdouts.push({ + id: 'holdout_second_id', + key: 'holdout_second', + status: 'Running', + includedFlags: [], + excludedFlags: [], + audienceIds: [], // no audience requirements + audienceConditions: [], + variations: [ + { + id: 'holdout_variation_second_id', + key: 'holdout_variation_second', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'holdout_variation_second_id', + endOfRange: 5000 + } + ] + }); + + const config = createProjectConfig(datafile); + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 20, // satisfies audience for holdout_running + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + + expect(variation.result).toEqual({ + experiment: config.holdoutIdMap && config.holdoutIdMap['holdout_running_id'], + variation: config.variationIdMap['holdout_variation_running_id'], + decisionSource: DECISION_SOURCES.HOLDOUT, + }); + }); + }); + }); + + describe('resolveVariationForFeatureList - sync', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should skip cmab experiments', async () => { + const { decisionService, cmabService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 15, // should satisfy audience condition for all experiments and targeted delivery + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'delivery_1') { + return { + result: '5004', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('sync', config, [feature], user, {}).get(); + + const variation = value[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['delivery_1'], + variation: config.variationIdMap['5004'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + expect(mockBucket).toHaveBeenCalledTimes(3); + verifyBucketCall(0, config, config.experimentKeyMap['exp_1'], user); + verifyBucketCall(1, config, config.experimentKeyMap['exp_2'], user); + verifyBucketCall(2, config, config.experimentKeyMap['delivery_1'], user); + + expect(cmabService.getDecision).not.toHaveBeenCalled(); + }); + + it('should ignore async user profile service', async () => { + const { decisionService, userProfileServiceAsync } = getDecisionService({ + userProfileService: false, + userProfileServiceAsync: true, + }); + + userProfileServiceAsync?.lookup.mockResolvedValue({ + user_id: 'tester', + experiment_bucket_map: { + '2002': { + variation_id: '5001', + }, + }, + }); + userProfileServiceAsync?.save.mockResolvedValue(null); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'exp_2') { + return { + result: '5002', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 55, // should satisfy audience condition for exp_2 and exp_3 + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('sync', config, [feature], user, {}).get(); + + const variation = value[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileServiceAsync?.lookup).not.toHaveBeenCalled(); + expect(userProfileServiceAsync?.save).not.toHaveBeenCalled(); + }); + + it('should use sync user profile service', async () => { + const { decisionService, userProfileService, userProfileServiceAsync } = getDecisionService({ + userProfileService: true, + userProfileServiceAsync: true, + }); + + userProfileService?.lookup.mockImplementation((userId: string) => { + if (userId === 'tester-1') { + return { + user_id: 'tester-1', + experiment_bucket_map: { + '2002': { + variation_id: '5001', + }, + }, + }; + } + return null; + }); + + userProfileServiceAsync?.lookup.mockResolvedValue(null); + userProfileServiceAsync?.save.mockResolvedValue(null); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey === 'exp_2') { + return { + result: '5002', + reasons: [], + }; + } + return { + result: null, + reasons: [], + }; + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user1 = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester-1', + attributes: { + age: 55, // should satisfy audience condition for exp_2 and exp_3 + }, + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('sync', config, [feature], user1, {}).get(); + + const variation = value[0]; + + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5001'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('tester-1'); + + const user2 = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester-2', + attributes: { + age: 55, // should satisfy audience condition for exp_2 and exp_3 + }, + }); + + const value2 = decisionService.resolveVariationsForFeatureList('sync', config, [feature], user2, {}).get(); + const variation2 = value2[0]; + expect(variation2.result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(2); + expect(userProfileService?.lookup).toHaveBeenNthCalledWith(2, 'tester-2'); + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'tester-2', + experiment_bucket_map: { + '2002': { + variation_id: '5002', + }, + }, + }); + + expect(userProfileServiceAsync?.lookup).not.toHaveBeenCalled(); + expect(userProfileServiceAsync?.save).not.toHaveBeenCalled(); + }); + }); + + describe('getVariationsForFeatureList', () => { + beforeEach(() => { + mockBucket.mockReset(); + }); + + it('should return correct results for all features in the feature list', () => { + const { decisionService } = getDecisionService(); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + op, + config, + experiment: any, + user, + decideOptions, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + return Value.of('sync', { + result: { variationKey: 'variation_2' }, + reasons: [], + }); + } else if (experiment.key === 'exp_4') { + return Value.of('sync', { + result: { variationKey: 'variation_flag_2' }, + reasons: [], + }); + } + return Value.of('sync', { + result: {}, + reasons: [], + }); + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + const featureList = [ + config.featureKeyMap['flag_1'], + config.featureKeyMap['flag_2'], + ]; + + const variations = decisionService.getVariationsForFeatureList(config, featureList, user); + + expect(variations[0].result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variations[1].result).toEqual({ + experiment: config.experimentKeyMap['exp_4'], + variation: config.variationIdMap['5100'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + const variations2 = decisionService.getVariationsForFeatureList(config, featureList.reverse(), user); + + expect(variations2[0].result).toEqual({ + experiment: config.experimentKeyMap['exp_4'], + variation: config.variationIdMap['5100'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variations2[1].result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + it('should batch user profile lookup and save', () => { + const { decisionService, userProfileService } = getDecisionService({ userProfileService: true }); + + const resolveVariationSpy = vi.spyOn(decisionService as any, 'resolveVariation') + .mockImplementation(( + op, + config, + experiment: any, + user, + decideOptions, + userProfileTracker: any, + ) => { + if (experiment.key === 'exp_2') { + userProfileTracker.userProfile[experiment.id] = { + variation_id: '5002', + }; + userProfileTracker.isProfileUpdated = true; + + return Value.of('sync', { + result: { variationKey: 'variation_2' }, + reasons: [], + }); + } else if (experiment.key === 'exp_4') { + userProfileTracker.userProfile[experiment.id] = { + variation_id: '5100', + }; + userProfileTracker.isProfileUpdated = true; + + return Value.of('sync', { + result: { variationKey: 'variation_flag_2' }, + reasons: [], + }); + } + return Value.of('sync', { + result: {}, + reasons: [], + }); + }); + + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + age: 40, + }, + }); + + const featureList = [ + config.featureKeyMap['flag_1'], + config.featureKeyMap['flag_2'], + ]; + + const variations = decisionService.getVariationsForFeatureList(config, featureList, user); + + expect(variations[0].result).toEqual({ + experiment: config.experimentKeyMap['exp_2'], + variation: config.variationIdMap['5002'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(variations[1].result).toEqual({ + experiment: config.experimentKeyMap['exp_4'], + variation: config.variationIdMap['5100'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + + expect(userProfileService?.lookup).toHaveBeenCalledTimes(1); + expect(userProfileService?.lookup).toHaveBeenCalledWith('tester'); + + expect(userProfileService?.save).toHaveBeenCalledTimes(1); + expect(userProfileService?.save).toHaveBeenCalledWith({ + user_id: 'tester', + experiment_bucket_map: { + '2002': { + variation_id: '5002', + }, + '2004': { + variation_id: '5100', + }, + }, + }); + }); + }); + + + describe('forced variation management', () => { + it('should return true for a valid forcedVariation in setForcedVariation', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(true); + }); + + it('should return the same variation from getVariation as was set in setVariation', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe('control'); + }); + + it('should return null from getVariation if no forced variation was set for a valid experimentKey', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + expect(config.experimentKeyMap['testExperiment']).toBeDefined(); + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + + expect(variation).toBe(null); + }); + + it('should return null from getVariation for an invalid experimentKey', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + expect(config.experimentKeyMap['definitely_not_valid_exp_key']).not.toBeDefined(); + const variation = decisionService.getForcedVariation(config, 'definitely_not_valid_exp_key', 'user1').result; + + expect(variation).toBe(null); + }); + + it('should return null when a forced decision is set on another experiment key', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + decisionService.setForcedVariation(config, 'testExperiment', 'user1', 'control'); + const variation = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + expect(variation).toBe(null); + }); + + it('should not set forced variation for an invalid variation key and return false', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const wasSet = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'definitely_not_valid_variation_key' + ); + + expect(wasSet).toBe(false); + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe(null); + }); + + it('should reset the forcedVariation if null is passed to setForcedVariation', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + + expect(didSetVariation).toBe(true); + + let variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe('control'); + + const didSetVariationAgain = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + null + ); + + expect(didSetVariationAgain).toBe(true); + + variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + expect(variation).toBe(null); + }); + + it('should be able to add variations for multiple experiments for one user', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + expect(didSetVariation2).toBe(true); + + const variation = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + const variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + expect(variation).toBe('control'); + expect(variation2).toBe('controlLaunched'); + }); + + it('should be able to forced variation to same experiment for multiple users', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user2', + 'variation' + ); + expect(didSetVariation2).toBe(true); + + const variationControl = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + const variationVariation = decisionService.getForcedVariation(config, 'testExperiment', 'user2').result; + + expect(variationControl).toBe('control'); + expect(variationVariation).toBe('variation'); + }); + + it('should be able to reset a variation for a user with multiple experiments', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + // Set the first time + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + expect(didSetVariation2).toBe(true); + + let variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + let variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe('control'); + expect(variation2).toBe('controlLaunched'); + + // Reset for one of the experiments + const didSetVariationAgain = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'variation' + ); + expect(didSetVariationAgain).toBe(true); + + variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe('variation'); + expect(variation2).toBe('controlLaunched'); + }); + + it('should be able to unset a variation for a user with multiple experiments', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + // Set the first time + const didSetVariation1 = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation1).toBe(true); + + const didSetVariation2 = decisionService.setForcedVariation( + config, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + expect(didSetVariation2).toBe(true); + + let variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + let variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe('control'); + expect(variation2).toBe('controlLaunched'); + + // Unset for one of the experiments + decisionService.setForcedVariation(config, 'testExperiment', 'user1', null); + + variation1 = decisionService.getForcedVariation(config, 'testExperiment', 'user1').result; + variation2 = decisionService.getForcedVariation(config, 'testExperimentLaunched', 'user1').result; + + expect(variation1).toBe(null); + expect(variation2).toBe('controlLaunched'); + }); + + it('should return false for an empty variation key', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation(config, 'testExperiment', 'user1', ''); + expect(didSetVariation).toBe(false); + }); + + it('should return null when a variation was previously set, and that variation no longer exists on the config object', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(true); + + const newDatafile = cloneDeep(testData); + // Remove 'control' variation from variations, traffic allocation, and datafile forcedVariations. + newDatafile.experiments[0].variations = [ + { + key: 'variation', + id: '111129', + }, + ]; + newDatafile.experiments[0].trafficAllocation = [ + { + entityId: '111129', + endOfRange: 9000, + }, + ]; + newDatafile.experiments[0].forcedVariations = { + user1: 'variation', + user2: 'variation', + }; + // Now the only variation in testExperiment is 'variation' + const newConfigObj = createProjectConfig(newDatafile); + const forcedVar = decisionService.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; + expect(forcedVar).toBe(null); + }); + + it("should return null when a variation was previously set, and that variation's experiment no longer exists on the config object", function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'testExperiment', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(true); + + const newConfigObj = createProjectConfig(cloneDeep(testDataWithFeatures)); + const forcedVar = decisionService.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; + expect(forcedVar).toBe(null); + }); + + it('should return false from setForcedVariation and not set for invalid experiment key', function() { + const config = createProjectConfig(cloneDeep(testData)); + const { decisionService } = getDecisionService(); + + const didSetVariation = decisionService.setForcedVariation( + config, + 'definitelyNotAValidExperimentKey', + 'user1', + 'control' + ); + expect(didSetVariation).toBe(false); + + const variation = decisionService.getForcedVariation( + config, + 'definitelyNotAValidExperimentKey', + 'user1' + ).result; + expect(variation).toBe(null); + }); + }); +}); diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js new file mode 100644 index 000000000..346814857 --- /dev/null +++ b/lib/core/decision_service/index.tests.js @@ -0,0 +1,1988 @@ +/** + * Copyright 2017-2022, 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sinon from 'sinon'; +import { assert } from 'chai'; +import cloneDeep from 'lodash/cloneDeep'; +import { sprintf } from '../../utils/fns'; + +import { createDecisionService } from './'; +import * as bucketer from '../bucketer'; +import * as bucketValueGenerator from '../bucketer/bucket_value_generator'; +import { + LOG_LEVEL, + DECISION_SOURCES, +} from '../../utils/enums'; +import { getForwardingEventProcessor } from '../../event_processor/event_processor_factory'; +import { createNotificationCenter } from '../../notification_center'; +import Optimizely from '../../optimizely'; +import OptimizelyUserContext from '../../optimizely_user_context'; +import projectConfig, { createProjectConfig } from '../../project_config/project_config'; +import AudienceEvaluator from '../audience_evaluator'; +import eventDispatcher from '../../event_processor/event_dispatcher/default_dispatcher.browser'; +import * as jsonSchemaValidator from '../../utils/json_schema_validator'; +import { getMockProjectConfigManager } from '../../tests/mock/mock_project_config_manager'; +import { Value } from '../../utils/promise/operation_value'; + +import { + getTestProjectConfig, + getTestProjectConfigWithFeatures, +} from '../../tests/test_data'; + +import { + USER_HAS_NO_FORCED_VARIATION, + VALID_BUCKETING_ID, + SAVED_USER_VARIATION, + SAVED_VARIATION_NOT_FOUND, +} from 'log_message'; + +import { + EXPERIMENT_NOT_RUNNING, + RETURNING_STORED_VARIATION, + USER_NOT_IN_EXPERIMENT, + USER_FORCED_IN_VARIATION, + EVALUATING_AUDIENCES_COMBINED, + AUDIENCE_EVALUATION_RESULT_COMBINED, + USER_IN_ROLLOUT, + USER_NOT_IN_ROLLOUT, + FEATURE_HAS_NO_EXPERIMENTS, + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + USER_BUCKETED_INTO_TARGETING_RULE, + NO_ROLLOUT_EXISTS, + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, +} from '../decision_service/index'; + +import { BUCKETING_ID_NOT_STRING, USER_PROFILE_LOOKUP_ERROR, USER_PROFILE_SAVE_ERROR } from 'error_message'; + +var testData = getTestProjectConfig(); +var testDataWithFeatures = getTestProjectConfigWithFeatures(); +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +describe('lib/core/decision_service', function() { + describe('APIs', function() { + var configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + var decisionServiceInstance; + var mockLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + var bucketerStub; + var experiment; + var user; + + beforeEach(function() { + bucketerStub = sinon.stub(bucketer, 'bucket'); + sinon.stub(mockLogger, 'info'); + sinon.stub(mockLogger, 'debug'); + sinon.stub(mockLogger, 'warn'); + sinon.stub(mockLogger, 'error'); + + decisionServiceInstance = createDecisionService({ + logger: mockLogger, + }); + }); + + afterEach(function() { + bucketer.bucket.restore(); + mockLogger.debug.restore(); + mockLogger.info.restore(); + mockLogger.warn.restore(); + mockLogger.error.restore(); + }); + + describe('#getVariation', function() { + it('should return the correct variation for the given experiment key and user ID for a running experiment', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'tester' + }); + var fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + experiment = configObj.experimentIdMap['111127']; + bucketerStub.returns(fakeDecisionResponse); // contains variation ID of the 'control' variation from `test_data` + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledOnce(bucketerStub); + }); + + it('should return the whitelisted variation if the user is whitelisted', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user2' + }); + experiment = configObj.experimentIdMap['122227']; + assert.strictEqual( + 'variationWithAudience', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.notCalled(bucketerStub); + assert.strictEqual(1, mockLogger.debug.callCount); + assert.strictEqual(1, mockLogger.info.callCount); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user2']); + + assert.deepEqual(mockLogger.info.args[0], [USER_FORCED_IN_VARIATION, 'user2', 'variationWithAudience']); + }); + + it('should return null if the user does not meet audience conditions', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user3' + }); + experiment = configObj.experimentIdMap['122227']; + assert.isNull( + decisionServiceInstance.getVariation(configObj, experiment, user, { foo: 'bar' }).result + ); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'user3']); + + assert.deepEqual(mockLogger.debug.args[1], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + + assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + + assert.deepEqual(mockLogger.info.args[1], [USER_NOT_IN_EXPERIMENT, 'user3', 'testExperimentWithAudiences']); + }); + + it('should return null if the experiment is not running', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1' + }); + experiment = configObj.experimentIdMap['133337']; + assert.isNull(decisionServiceInstance.getVariation(configObj, experiment, user).result); + sinon.assert.notCalled(bucketerStub); + assert.strictEqual(1, mockLogger.info.callCount); + + assert.deepEqual(mockLogger.info.args[0], [EXPERIMENT_NOT_RUNNING, 'testExperimentNotRunning']); + }); + + describe('when attributes.$opt_experiment_bucket_map is supplied', function() { + it('should respect the sticky bucketing information for attributes', function() { + var fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + experiment = configObj.experimentIdMap['111127']; + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation from `test_data` + var attributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + attributes, + }); + + assert.strictEqual( + 'variation', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.notCalled(bucketerStub); + }); + }); + + describe('when a user profile service is provided', function() { + var fakeDecisionResponse = { + result: '111128', + reasons: [], + }; + var userProfileServiceInstance = null; + var userProfileLookupStub; + var userProfileSaveStub; + var fakeDecisionWhitelistedVariation = { + result: null, + reasons: [], + } + beforeEach(function() { + userProfileServiceInstance = { + lookup: function() {}, + save: function() {}, + }; + + decisionServiceInstance = createDecisionService({ + logger: mockLogger, + userProfileService: userProfileServiceInstance, + }); + userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); + userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); + sinon.stub(decisionServiceInstance, 'getWhitelistedVariation').returns(fakeDecisionWhitelistedVariation); + }); + + afterEach(function() { + userProfileServiceInstance.lookup.restore(); + userProfileServiceInstance.save.restore(); + decisionServiceInstance.getWhitelistedVariation.restore(); + }); + + it('should return the previously bucketed variation', function() { + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + experiment = configObj.experimentIdMap['111127']; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.notCalled(bucketerStub); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'control', 'testExperiment', 'decision_service_user']); + }); + + it('should bucket if there was no prevously bucketed variation', function() { + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: {}, + }); + experiment = configObj.experimentIdMap['111127']; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.calledOnce(bucketerStub); + // make sure we save the decision + sinon.assert.calledWith(userProfileSaveStub, { + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should bucket if the user profile service returns null', function() { + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + userProfileLookupStub.returns(null); + experiment = configObj.experimentIdMap['111127']; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.calledOnce(bucketerStub); + // make sure we save the decision + sinon.assert.calledWith(userProfileSaveStub, { + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should re-bucket if the stored variation is no longer valid', function() { + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: 'not valid variation', + }, + }, + }); + experiment = configObj.experimentIdMap['111127']; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.calledOnce(bucketerStub); + // assert.strictEqual( + // buildLogMessageFromArgs(mockLogger.log.args[0]), + // 'DECISION_SERVICE: User decision_service_user is not in the forced variation map.' + // ); + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + sinon.assert.calledWith( + mockLogger.info, + SAVED_VARIATION_NOT_FOUND, + 'decision_service_user', + 'not valid variation', + 'testExperiment' + ); + + // make sure we save the decision + sinon.assert.calledWith(userProfileSaveStub, { + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + it('should store the bucketed variation for the user', function() { + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: {}, // no decisions for user + }); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + experiment = configObj.experimentIdMap['111127']; + + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.calledOnce(bucketerStub); + + sinon.assert.calledWith(userProfileServiceInstance.save, { + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.info.lastCall.args, [SAVED_USER_VARIATION, 'decision_service_user']); + }); + + it('should log an error message if "lookup" throws an error', function() { + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + userProfileLookupStub.throws(new Error('I am an error')); + experiment = configObj.experimentIdMap['111127']; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing + + assert.deepEqual(mockLogger.error.args[0], [USER_PROFILE_LOOKUP_ERROR, 'decision_service_user', 'I am an error']); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + }); + + it('should log an error message if "save" throws an error', function() { + bucketerStub.returns(fakeDecisionResponse); // ID of the 'control' variation + userProfileLookupStub.returns(null); + userProfileSaveStub.throws(new Error('I am an error')); + experiment = configObj.experimentIdMap['111127']; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + }); + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing + + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.error.args[0], [USER_PROFILE_SAVE_ERROR, 'decision_service_user', 'I am an error']); + + // make sure that we save the decision + sinon.assert.calledWith(userProfileSaveStub, { + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', + }, + }, + }); + }); + + describe('when passing `attributes.$opt_experiment_bucket_map`', function() { + it('should respect attributes over the userProfileService for the matching experiment id', function() { + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + + var attributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + experiment = configObj.experimentIdMap['111127']; + + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + attributes, + }); + + assert.strictEqual( + 'variation', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.notCalled(bucketerStub); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); + }); + + it('should ignore attributes for a different experiment id', function() { + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '111127': { + // 'testExperiment' ID + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + + experiment = configObj.experimentIdMap['111127']; + + var attributes = { + $opt_experiment_bucket_map: { + '122227': { + // other experiment ID + variation_id: '122229', // ID of the 'variationWithAudience' variation + }, + }, + }; + + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + attributes, + }); + + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.notCalled(bucketerStub); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'control', 'testExperiment', 'decision_service_user']); + }); + + it('should use attributes when the userProfileLookup variations for other experiments', function() { + userProfileLookupStub.returns({ + user_id: 'decision_service_user', + experiment_bucket_map: { + '122227': { + // other experiment ID + variation_id: '122229', // ID of the 'variationWithAudience' variation + }, + }, + }); + + experiment = configObj.experimentIdMap['111127']; + + var attributes = { + $opt_experiment_bucket_map: { + '111127': { + // 'testExperiment' ID + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + attributes, + }); + + assert.strictEqual( + 'variation', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.notCalled(bucketerStub); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); + }); + + it('should use attributes when the userProfileLookup returns null', function() { + userProfileLookupStub.returns(null); + + experiment = configObj.experimentIdMap['111127']; + + var attributes = { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // ID of the 'variation' variation + }, + }, + }; + + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'decision_service_user', + attributes, + }); + + assert.strictEqual( + 'variation', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); + sinon.assert.notCalled(bucketerStub); + + assert.deepEqual(mockLogger.debug.args[0], [USER_HAS_NO_FORCED_VARIATION, 'decision_service_user']); + + assert.deepEqual(mockLogger.info.args[0], [RETURNING_STORED_VARIATION, 'variation', 'testExperiment', 'decision_service_user']); + }); + }); + }); + }); + + describe('buildBucketerParams', function() { + it('should return params object with correct properties', function() { + experiment = configObj.experimentIdMap['111127']; + var bucketerParams = decisionServiceInstance.buildBucketerParams( + configObj, + experiment, + 'testUser', + 'testUser' + ); + + var expectedParams = { + bucketingId: 'testUser', + experimentKey: 'testExperiment', + userId: 'testUser', + experimentId: '111127', + trafficAllocationConfig: [ + { + entityId: '111128', + endOfRange: 4000, + }, + { + entityId: '111129', + endOfRange: 9000, + }, + ], + variationIdMap: configObj.variationIdMap, + logger: mockLogger, + experimentIdMap: configObj.experimentIdMap, + experimentKeyMap: configObj.experimentKeyMap, + groupIdMap: configObj.groupIdMap, + validateEntity: true, + }; + + assert.deepEqual(bucketerParams, expectedParams); + }); + }); + + describe('checkIfUserIsInAudience', function() { + var __audienceEvaluateSpy; + + beforeEach(function() { + __audienceEvaluateSpy = sinon.spy(AudienceEvaluator.prototype, 'evaluate'); + }); + + afterEach(function() { + __audienceEvaluateSpy.restore(); + }); + + it('should return decision response with result true when audience conditions are met', function() { + experiment = configObj.experimentIdMap['122227']; + assert.isTrue( + decisionServiceInstance.checkIfUserIsInAudience( + configObj, + experiment, + "experiment", + { getAttributes: () => ({ browser_type: 'firefox' }) }, + '' + ).result + ); + + assert.deepEqual(mockLogger.debug.args[0], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + + assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'TRUE']); + }); + + it('should return decision response with result true when experiment has no audience', function() { + experiment = configObj.experimentIdMap['111127']; + assert.isTrue( + decisionServiceInstance.checkIfUserIsInAudience( + configObj, + experiment, + 'experiment', + {}, + '' + ).result + ); + assert.isTrue(__audienceEvaluateSpy.alwaysReturned(true)); + + assert.deepEqual(mockLogger.debug.args[0], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperiment', JSON.stringify([])]); + + assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperiment', 'TRUE']); + }); + + it('should return decision response with result false when audience conditions can not be evaluated', function() { + experiment = configObj.experimentIdMap['122227']; + assert.isFalse( + decisionServiceInstance.checkIfUserIsInAudience( + configObj, + experiment, + "experiment", + {}, + '' + ).result + ); + assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false)); + + assert.deepEqual(mockLogger.debug.args[0], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + + assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + }); + + it('should return decision response with result false when audience conditions are not met', function() { + experiment = configObj.experimentIdMap['122227']; + assert.isFalse( + decisionServiceInstance.checkIfUserIsInAudience( + configObj, + experiment, + "experiment", + { browser_type: 'chrome' }, + '' + ).result + ); + assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false)); + + + assert.deepEqual(mockLogger.debug.args[0], [EVALUATING_AUDIENCES_COMBINED, 'experiment', 'testExperimentWithAudiences', JSON.stringify(["11154"])]); + + assert.deepEqual(mockLogger.info.args[0], [AUDIENCE_EVALUATION_RESULT_COMBINED, 'experiment', 'testExperimentWithAudiences', 'FALSE']); + }); + }); + + describe('getWhitelistedVariation', function() { + it('should return forced variation ID if forced variation is provided for the user ID', function() { + var testExperiment = configObj.experimentKeyMap['testExperiment']; + var expectedVariation = configObj.variationIdMap['111128']; + assert.strictEqual( + decisionServiceInstance.getWhitelistedVariation(testExperiment, 'user1').result, + expectedVariation + ); + }); + + it('should return null if forced variation is not provided for the user ID', function() { + var testExperiment = configObj.experimentKeyMap['testExperiment']; + assert.isNull(decisionServiceInstance.getWhitelistedVariation(testExperiment, 'notInForcedVariations').result); + }); + }); + + describe('getForcedVariation', function() { + it('should return null for valid experimentKey, not set', function() { + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + assert.strictEqual(variation, null); + }); + + it('should return null for invalid experimentKey, not set', function() { + var variation = decisionServiceInstance.getForcedVariation(configObj, 'definitely_not_valid_exp_key', 'user1').result; + assert.strictEqual(variation, null); + }); + + it('should return null for invalid experimentKey when a variation was previously successfully forced on another experiment for the same user', function() { + decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', 'control'); + var variation = decisionServiceInstance.getForcedVariation(configObj, 'definitely_not_valid_exp_key', 'user1').result; + assert.strictEqual(variation, null); + }); + + it('should return null for valid experiment key, not set on this experiment key, but set on another experiment key', function() { + decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', 'control'); + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1').result; + assert.strictEqual(variation, null); + }); + }); + + describe('#setForcedVariation', function() { + it('should return true for a valid forcedVariation in setForcedVariation', function() { + var didSetVariation = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation, true); + }); + + it('should return the same variation from getVariation as was set in setVariation', function() { + decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', 'control'); + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + assert.strictEqual(variation, 'control'); + }); + + it('should not set for an invalid variation key', function() { + decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'definitely_not_valid_variation_key' + ); + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + assert.strictEqual(variation, null); + }); + + it('should reset the forcedVariation if passed null', function() { + var didSetVariation = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation, true); + + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + assert.strictEqual(variation, 'control' ); + + var didSetVariationAgain = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + null + ); + assert.strictEqual(didSetVariationAgain, true); + + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + assert.strictEqual(variation, null); + }); + + it('should be able to add variations for multiple experiments for one user', function() { + var didSetVariation1 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation1, true); + + var didSetVariation2 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + var variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1').result; + assert.strictEqual(variation, 'control'); + assert.strictEqual(variation2, 'controlLaunched'); + }); + + it('should be able to add experiments for multiple users', function() { + var didSetVariation1 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation1, true); + + var didSetVariation2 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user2', + 'variation' + ); + assert.strictEqual(didSetVariation2, true); + + var variationControl = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + var variationVariation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user2').result; + + assert.strictEqual(variationControl, 'control'); + assert.strictEqual(variationVariation, 'variation'); + }); + + it('should be able to reset a variation for a user with multiple experiments', function() { + //set the first time + var didSetVariation1 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation1, true); + + var didSetVariation2 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + let variation1 = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + let variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1').result; + + assert.strictEqual(variation1, 'control'); + assert.strictEqual(variation2, 'controlLaunched'); + + //reset for one of the experiments + var didSetVariationAgain = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'variation' + ); + assert.strictEqual(didSetVariationAgain, true); + + variation1 = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1').result; + + assert.strictEqual(variation1, 'variation'); + assert.strictEqual(variation2, 'controlLaunched'); + }); + + it('should be able to unset a variation for a user with multiple experiments', function() { + //set the first time + var didSetVariation1 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation1, true); + + var didSetVariation2 = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperimentLaunched', + 'user1', + 'controlLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + let variation1 = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + let variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1').result; + + assert.strictEqual(variation1, 'control'); + assert.strictEqual(variation2, 'controlLaunched'); + + //reset for one of the experiments + decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', null); + assert.strictEqual(didSetVariation1, true); + + variation1 = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1').result; + variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1').result; + + assert.strictEqual(variation1, null); + assert.strictEqual(variation2, 'controlLaunched'); + }); + + it('should return false for an empty variation key', function() { + var didSetVariation = decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', ''); + assert.strictEqual(didSetVariation, false); + }); + + it('should return null when a variation was previously set, and that variation no longer exists on the config object', function() { + var didSetVariation = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation, true); + var newDatafile = cloneDeep(testData); + // Remove 'control' variation from variations, traffic allocation, and datafile forcedVariations. + newDatafile.experiments[0].variations = [ + { + key: 'variation', + id: '111129', + }, + ]; + newDatafile.experiments[0].trafficAllocation = [ + { + entityId: '111129', + endOfRange: 9000, + }, + ]; + newDatafile.experiments[0].forcedVariations = { + user1: 'variation', + user2: 'variation', + }; + // Now the only variation in testExperiment is 'variation' + var newConfigObj = projectConfig.createProjectConfig(newDatafile); + var forcedVar = decisionServiceInstance.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; + assert.strictEqual(forcedVar, null); + }); + + it("should return null when a variation was previously set, and that variation's experiment no longer exists on the config object", function() { + var didSetVariation = decisionServiceInstance.setForcedVariation( + configObj, + 'testExperiment', + 'user1', + 'control' + ); + assert.strictEqual(didSetVariation, true); + var newConfigObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures)); + var forcedVar = decisionServiceInstance.getForcedVariation(newConfigObj, 'testExperiment', 'user1').result; + assert.strictEqual(forcedVar, null); + }); + + it('should return false from setForcedVariation and not set for invalid experiment key', function() { + var didSetVariation = decisionServiceInstance.setForcedVariation( + configObj, + 'definitelyNotAValidExperimentKey', + 'user1', + 'definitely_not_valid_variation_key' + ); + assert.strictEqual(didSetVariation, false); + var variation = decisionServiceInstance.getForcedVariation( + configObj, + 'definitelyNotAValidExperimentKey', + 'user1' + ).result; + assert.strictEqual(variation, null); + }); + }); + }); + + // TODO: Move tests that test methods of Optimizely to lib/optimizely/index.tests.js + describe('when a bucketingID is provided', function() { + var configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.DEBUG, + logToConsole: false, + }); + var optlyInstance; + var user; + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(cloneDeep(testData)) + }), + jsonSchemaValidator: jsonSchemaValidator, + isValidInstance: true, + logger: createdLogger, + eventProcessor: getForwardingEventProcessor(eventDispatcher), + notificationCenter: createNotificationCenter(createdLogger), + }); + + sinon.stub(eventDispatcher, 'dispatchEvent'); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.restore(); + }); + + var testUserAttributes = { + browser_type: 'firefox', + }; + var userAttributesWithBucketingId = { + browser_type: 'firefox', + $opt_bucketing_id: '123456789', + }; + var invalidUserAttributesWithBucketingId = { + browser_type: 'safari', + $opt_bucketing_id: 'testBucketingIdControl!', + }; + + it('confirm normal bucketing occurs before setting bucketingId', function() { + assert.strictEqual('variation', optlyInstance.getVariation('testExperiment', 'test_user', testUserAttributes)); + }); + + it('confirm valid bucketing with bucketing ID set in attributes', function() { + assert.strictEqual( + 'variationWithAudience', + optlyInstance.getVariation('testExperimentWithAudiences', 'test_user', userAttributesWithBucketingId) + ); + }); + + it('check invalid audience with bucketingId', function() { + assert.strictEqual( + null, + optlyInstance.getVariation('testExperimentWithAudiences', 'test_user', invalidUserAttributesWithBucketingId) + ); + }); + + it('test that an experiment that is not running returns a null variation', function() { + assert.strictEqual( + null, + optlyInstance.getVariation('testExperimentNotRunning', 'test_user', userAttributesWithBucketingId) + ); + }); + + it('test that an invalid experiment key gets a null variation', function() { + assert.strictEqual( + null, + optlyInstance.getVariation('invalidExperiment', 'test_user', userAttributesWithBucketingId) + ); + }); + + it('check forced variation', function() { + assert.isTrue( + optlyInstance.setForcedVariation('testExperiment', 'test_user', 'control'), + sprintf('Set variation to "%s" failed', 'control') + ); + assert.strictEqual( + 'control', + optlyInstance.getVariation('testExperiment', 'test_user', userAttributesWithBucketingId) + ); + }); + + it('check whitelisted variation', function() { + assert.strictEqual( + 'control', + optlyInstance.getVariation('testExperiment', 'user1', userAttributesWithBucketingId) + ); + }); + + it('check user profile', function() { + var userProfileLookupStub; + var userProfileServiceInstance = { + lookup: function() {}, + }; + userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); + userProfileLookupStub.returns({ + user_id: 'test_user', + experiment_bucket_map: { + '111127': { + variation_id: '111128', // ID of the 'control' variation + }, + }, + }); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'test_user', + attributes: userAttributesWithBucketingId, + }); + var experiment = configObj.experimentIdMap['111127']; + + var decisionServiceInstance = createDecisionService({ + logger: createdLogger, + userProfileService: userProfileServiceInstance, + }); + + assert.strictEqual( + 'control', + decisionServiceInstance.getVariation(configObj, experiment, user).result + ); + sinon.assert.calledWithExactly(userProfileLookupStub, 'test_user'); + }); + }); + + describe('getBucketingId', function() { + var configObj; + var decisionService; + var mockLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + var userId = 'testUser1'; + var userAttributesWithBucketingId = { + browser_type: 'firefox', + $opt_bucketing_id: '123456789', + }; + var userAttributesWithInvalidBucketingId = { + browser_type: 'safari', + $opt_bucketing_id: 50, + }; + + beforeEach(function() { + sinon.stub(mockLogger, 'debug'); + sinon.stub(mockLogger, 'info'); + sinon.stub(mockLogger, 'warn'); + sinon.stub(mockLogger, 'error'); + + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + decisionService = createDecisionService({ + logger: mockLogger, + }); + }); + + afterEach(function() { + mockLogger.debug.restore(); + mockLogger.info.restore(); + mockLogger.warn.restore(); + mockLogger.error.restore(); + }); + + it('should return userId if bucketingId is not defined in user attributes', function() { + assert.strictEqual(userId, decisionService.getBucketingId(userId, null)); + assert.strictEqual(userId, decisionService.getBucketingId(userId, { browser_type: 'safari' })); + }); + + it('should log warning in case of invalid bucketingId', function() { + assert.strictEqual(userId, decisionService.getBucketingId(userId, userAttributesWithInvalidBucketingId)); + assert.deepEqual(mockLogger.warn.args[0], [BUCKETING_ID_NOT_STRING]); + }); + + it('should return correct bucketingId when provided in attributes', function() { + assert.strictEqual('123456789', decisionService.getBucketingId(userId, userAttributesWithBucketingId)); + assert.strictEqual(1, mockLogger.debug.callCount); + assert.deepEqual(mockLogger.debug.args[0], [VALID_BUCKETING_ID, '123456789']); + }); + }); + + describe('feature management', function() { + describe('#getVariationForFeature', function() { + var configObj; + var decisionServiceInstance; + var sandbox; + var mockLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + var fakeDecisionResponseWithArgs; + var fakeDecisionResponse = Value.of('sync', { + result: {}, + reasons: [], + }); + var user; + beforeEach(function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures)); + sandbox = sinon.sandbox.create(); + sandbox.stub(mockLogger, 'debug'); + sandbox.stub(mockLogger, 'info'); + sandbox.stub(mockLogger, 'warn'); + sandbox.stub(mockLogger, 'error'); + + decisionServiceInstance = createDecisionService({ + logger: mockLogger, + }); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('feature attached to an experiment, and not attached to a rollout', function() { + var feature; + beforeEach(function() { + feature = configObj.featureKeyMap.test_feature_for_experiment; + }); + + describe('user bucketed into this experiment', function() { + var getVariationStub; + var experiment; + beforeEach(function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { + test_attribute: 'test_value', + }, + }); + fakeDecisionResponseWithArgs = Value.of('sync', { + result: { variationKey: 'variation' }, + reasons: [], + }); + experiment = configObj.experimentIdMap['594098']; + getVariationStub = sandbox.stub(decisionServiceInstance, 'resolveVariation'); + getVariationStub.returns(fakeDecisionResponse); + getVariationStub.withArgs('sync', configObj, experiment, user, sinon.match.any, sinon.match.any).returns(fakeDecisionResponseWithArgs); + }); + + it('returns a decision with a variation in the experiment the feature is attached to', function() { + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + const expectedDecision = { + cmabUuid: undefined, + experiment: configObj.experimentIdMap['594098'], + variation: configObj.variationIdMap['594096'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWith( + getVariationStub, + 'sync', + configObj, + experiment, + user, + sinon.match.any, + sinon.match.any + ); + }); + }); + + describe('user not bucketed into this experiment', function() { + var getVariationStub; + beforeEach(function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + }); + getVariationStub = sandbox.stub(decisionServiceInstance, 'resolveVariation'); + getVariationStub.returns(fakeDecisionResponse); + }); + + it('returns a decision with no variation and source rollout', function() { + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + assert.deepEqual(decision, expectedDecision); + + assert.deepEqual(mockLogger.debug.lastCall.args, [USER_NOT_IN_ROLLOUT, 'user1', 'test_feature_for_experiment']); + }); + }); + }); + + describe('feature attached to an experiment in a group, and not attached to a rollout', function() { + var feature; + beforeEach(function() { + feature = configObj.featureKeyMap.feature_with_group; + }); + + describe('user bucketed into an experiment in the group', function() { + var getVariationStub; + var user; + beforeEach(function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + }); + fakeDecisionResponseWithArgs = Value.of('sync', { + result: { variationKey: 'var' }, + reasons: [], + }); + getVariationStub = sandbox.stub(decisionServiceInstance, 'resolveVariation'); + getVariationStub.returns(fakeDecisionResponseWithArgs); + getVariationStub.withArgs(configObj, 'exp_with_group', user).returns(fakeDecisionResponseWithArgs); + }); + + it('returns a decision with a variation in an experiment in a group', function() { + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + cmabUuid: undefined, + experiment: configObj.experimentIdMap['595010'], + variation: configObj.variationIdMap['595008'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + + assert.deepEqual(decision, expectedDecision); + }); + }); + + describe('user not bucketed into an experiment in the group', function() { + var getVariationStub; + var user; + beforeEach(function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + }); + getVariationStub = sandbox.stub(decisionServiceInstance, 'resolveVariation'); + getVariationStub.returns(fakeDecisionResponse); + }); + + it('returns a decision with no experiment, no variation and source rollout', function() { + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + assert.deepEqual(decision, expectedDecision); + + assert.deepEqual(mockLogger.debug.lastCall.args, [USER_NOT_IN_ROLLOUT, 'user1', 'feature_with_group']); + }); + + it('returns null decision for group experiment not referenced by the feature', function() { + var noTrafficExpFeature = configObj.featureKeyMap.feature_exp_no_traffic; + var decision = decisionServiceInstance.getVariationForFeature(configObj, noTrafficExpFeature, user).result; + var expectedDecision = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + NO_ROLLOUT_EXISTS, + 'feature_exp_no_traffic' + ); + }); + }); + }); + + describe('feature attached to a rollout', function() { + var feature; + var bucketStub; + beforeEach(function() { + feature = configObj.featureKeyMap.test_feature; + bucketStub = sandbox.stub(bucketer, 'bucket'); + }); + + describe('user bucketed into an audience targeting rule', function() { + beforeEach(function() { + fakeDecisionResponse = { + result: '594032', // ID of variation in rollout experiment - audience targeting rule for 'test_audience' + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse) + }); + + it('returns a decision with a variation and experiment from the audience targeting rule', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: configObj.experimentIdMap['594031'], + variation: configObj.variationIdMap['594032'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, + 'user1', 1 + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_BUCKETED_INTO_TARGETING_RULE, + 'user1', 1 + + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_IN_ROLLOUT, + 'user1', 'test_feature' + ); + }); + }); + + describe('user bucketed into everyone else targeting rule', function() { + beforeEach(function() { + fakeDecisionResponse = { + result: '594038', // ID of variation in rollout experiment - everyone else targeting rule + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse) + }); + + it('returns a decision with a variation and experiment from the everyone else targeting rule', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: {}, + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: configObj.experimentIdMap['594037'], + variation: configObj.variationIdMap['594038'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + 'user1', 1 + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_BUCKETED_INTO_TARGETING_RULE, + 'user1', 'Everyone Else' + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_IN_ROLLOUT, + 'user1', 'test_feature' + ); + }); + }); + + describe('user not bucketed into audience targeting rule or everyone else rule', function() { + beforeEach(function() { + fakeDecisionResponse = { + result: null, + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse) + }); + + it('returns a decision with no variation, no experiment and source rollout', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + 'user1', 1 + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_NOT_IN_ROLLOUT, + 'user1', 'test_feature' + ); + }); + }); + + describe('user excluded from audience targeting rule due to traffic allocation, and bucketed into everyone else', function() { + beforeEach(function() { + fakeDecisionResponse = { + result: null, + reasons: [], + }; + fakeDecisionResponseWithArgs = { + result: '594038', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); // returns no variation for other calls + bucketStub + .withArgs( + sinon.match({ + experimentKey: '594037', + }) + ) + .returns(fakeDecisionResponseWithArgs); // returns variation from everyone else targeitng rule when called with everyone else experiment key; + }); + + it('returns a decision with a variation and experiment from the everyone else targeting rule', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { test_attribute: 'test_value' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: configObj.experimentIdMap['594037'], + variation: configObj.variationIdMap['594038'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, + 'user1', 1 + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + 'user1', 1 + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_BUCKETED_INTO_TARGETING_RULE, + 'user1', 'Everyone Else' + ); + }); + }); + }); + + describe('feature attached to both an experiment and a rollout', function() { + var feature; + var getVariationStub; + var bucketStub; + fakeDecisionResponse = Value.of('sync', { + result: {}, + reasons: [], + }); + var fakeBucketStubDecisionResponse = { + result: '599057', + reasons: [], + } + beforeEach(function() { + feature = configObj.featureKeyMap.shared_feature; + getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation'); + getVariationStub.returns(fakeDecisionResponse); // No variation returned by getVariation + bucketStub = sandbox.stub(bucketer, 'bucket'); + bucketStub.returns(fakeBucketStubDecisionResponse); // Id of variation in rollout of shared feature + }); + + it('can bucket a user into the rollout when the user is not bucketed into the experiment', function() { + // No attributes passed to the user context, so user is not in the audience for the experiment + // It should fall through to the rollout + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1' + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: configObj.experimentIdMap['599056'], + variation: configObj.variationIdMap['599057'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_BUCKETED_INTO_TARGETING_RULE, + 'user1', 'Everyone Else' + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + USER_IN_ROLLOUT, + 'user1', 'shared_feature' + ); + }); + }); + + describe('feature not attached to an experiment or a rollout', function() { + var feature; + beforeEach(function() { + feature = configObj.featureKeyMap.unused_flag; + }); + + it('returns a decision with no variation, no experiment and source rollout', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1' + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedDecision = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledWithExactly( + mockLogger.debug, + FEATURE_HAS_NO_EXPERIMENTS, + 'unused_flag' + ); + sinon.assert.calledWithExactly( + mockLogger.debug, + NO_ROLLOUT_EXISTS, + 'unused_flag' + ); + }); + }); + + describe('feature attached to exclusion group', function() { + var feature; + var generateBucketValueStub; + beforeEach(function() { + feature = configObj.featureKeyMap.test_feature_in_exclusion_group; + generateBucketValueStub = sandbox.stub(bucketValueGenerator, 'generateBucketValue'); + }); + + it('returns a decision with a variation in mutex group bucket less than 2500', function() { + generateBucketValueStub.returns(2400); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromKey(configObj, 'group_2_exp_1'); + var expectedDecision = { + cmabUuid: undefined, + experiment: expectedExperiment, + variation: { + id: '38901', + key: 'var_1', + featureEnabled: false, + }, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user142222' + ); + }); + + it('returns a decision with a variation in mutex group bucket range 2500 to 5000', function() { + generateBucketValueStub.returns(4000); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromKey(configObj, 'group_2_exp_2'); + var expectedDecision = { + cmabUuid: undefined, + experiment: expectedExperiment, + variation: { + id: '38905', + key: 'var_1', + featureEnabled: false, + }, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user142223' + ); + }); + + it('returns a decision with a variation in mutex group bucket range 5000 to 7500', function() { + generateBucketValueStub.returns(6500); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromKey(configObj, 'group_2_exp_3'); + var expectedDecision = { + cmabUuid: undefined, + experiment: expectedExperiment, + variation: { + id: '38906', + key: 'var_1', + featureEnabled: false, + }, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user142224' + ); + }); + + it('returns a decision with variation and source rollout in mutex group bucket greater than 7500', function() { + generateBucketValueStub.returns(8000); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066'); + var expectedDecision = { + experiment: expectedExperiment, + variation: configObj.variationIdMap['594067'], + decisionSource: DECISION_SOURCES.ROLLOUT, + } + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1594066' + ); + }); + + it('returns a decision with variation for rollout in mutex group with audience mismatch', function() { + generateBucketValueStub.returns(2400); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment_invalid' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066', mockLogger); + var expectedDecision = { + experiment: expectedExperiment, + variation: configObj.variationIdMap['594067'], + decisionSource: DECISION_SOURCES.ROLLOUT, + } + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1594066' + ); + }); + }); + + describe('feature attached to multiple experiments', function() { + var feature; + var generateBucketValueStub; + beforeEach(function() { + feature = configObj.featureKeyMap.test_feature_in_multiple_experiments; + generateBucketValueStub = sandbox.stub(bucketValueGenerator, 'generateBucketValue'); + }); + + it('returns a decision with a variation in mutex group bucket less than 2500', function() { + generateBucketValueStub.returns(2400); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromKey(configObj, 'test_experiment3'); + var expectedDecision = { + cmabUuid: undefined, + experiment: expectedExperiment, + variation: { + id: '222239', + key: 'control', + featureEnabled: false, + variables: [], + }, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1111134' + ); + }); + + it('returns a decision with a variation in mutex group bucket range 2500 to 5000', function() { + generateBucketValueStub.returns(4000); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromKey(configObj, 'test_experiment4'); + var expectedDecision = { + cmabUuid: undefined, + experiment: expectedExperiment, + variation: { + id: '222240', + key: 'control', + featureEnabled: false, + variables: [], + }, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1111135' + ); + }); + + it('returns a decision with a variation in mutex group bucket range 5000 to 7500', function() { + generateBucketValueStub.returns(6500); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromKey(configObj, 'test_experiment5'); + var expectedDecision = { + cmabUuid: undefined, + experiment: expectedExperiment, + variation: { + id: '222241', + key: 'control', + featureEnabled: false, + variables: [], + }, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1111136' + ); + }); + + it('returns a decision with variation and source rollout in mutex group bucket greater than 7500', function() { + generateBucketValueStub.returns(8000); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066'); + var expectedDecision = { + experiment: expectedExperiment, + variation: configObj.variationIdMap['594067'], + decisionSource: DECISION_SOURCES.ROLLOUT, + } + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1594066' + ); + }); + + it('returns a decision with variation for rollout in mutex group bucket range 2500 to 5000', function() { + generateBucketValueStub.returns(4000); + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'user1', + attributes: { experiment_attr: 'group_experiment_invalid' } + }); + var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, user).result; + var expectedExperiment = projectConfig.getExperimentFromId(configObj, '594066', mockLogger); + var expectedDecision = { + experiment: expectedExperiment, + variation: configObj.variationIdMap['594067'], + decisionSource: DECISION_SOURCES.ROLLOUT, + } + assert.deepEqual(decision, expectedDecision); + + sinon.assert.calledWithExactly( + generateBucketValueStub, + 'user1594066' + ); + }); + }); + }); + + describe('getVariationForRollout', function() { + var feature; + var configObj; + var decisionService; + var buildBucketerParamsSpy; + var user; + + beforeEach(function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures)); + feature = configObj.featureKeyMap.test_feature; + decisionService = createDecisionService({ + logger: createLogger({ logLevel: LOG_LEVEL.INFO }), + }); + buildBucketerParamsSpy = sinon.spy(decisionService, 'buildBucketerParams'); + }); + + afterEach(function() { + buildBucketerParamsSpy.restore(); + }); + + it('should call buildBucketerParams with user Id when bucketing Id is not provided in the attributes', function() { + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'testUser', + attributes: { test_attribute: 'test_value' } + }); + decisionService.getVariationForRollout(configObj, feature, user).result; + + sinon.assert.callCount(buildBucketerParamsSpy, 2); + sinon.assert.calledWithExactly(buildBucketerParamsSpy, configObj, configObj.experimentIdMap['594031'], 'testUser', 'testUser'); + sinon.assert.calledWithExactly(buildBucketerParamsSpy, configObj, configObj.experimentIdMap['594037'], 'testUser', 'testUser'); + }); + + it('should call buildBucketerParams with bucketing Id when bucketing Id is provided in the attributes', function() { + var attributes = { + test_attribute: 'test_value', + $opt_bucketing_id: 'abcdefg', + }; + user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: {}, + userId: 'testUser', + attributes, + }); + decisionService.getVariationForRollout(configObj, feature, user).result; + + sinon.assert.callCount(buildBucketerParamsSpy, 2); + sinon.assert.calledWithExactly(buildBucketerParamsSpy, configObj, configObj.experimentIdMap['594031'], 'abcdefg', 'testUser'); + sinon.assert.calledWithExactly(buildBucketerParamsSpy, configObj, configObj.experimentIdMap['594037'], 'abcdefg', 'testUser'); + }); + }); + }); +}); diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts new file mode 100644 index 000000000..057a0e129 --- /dev/null +++ b/lib/core/decision_service/index.ts @@ -0,0 +1,1697 @@ +/** + * Copyright 2017-2022, 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from '../../logging/logger' +import { bucket } from '../bucketer'; +import { + AUDIENCE_EVALUATION_TYPES, + CONTROL_ATTRIBUTES, + DECISION_SOURCES, + DecisionSource, +} from '../../utils/enums'; +import { + getAudiencesById, + getExperimentFromId, + getExperimentFromKey, + getFlagVariationByKey, + getVariationIdFromExperimentAndVariationKey, + getVariationFromId, + getVariationKeyFromId, + isActive, + ProjectConfig, + getHoldoutsForFlag, +} from '../../project_config/project_config'; +import { AudienceEvaluator, createAudienceEvaluator } from '../audience_evaluator'; +import * as stringValidator from '../../utils/string_value_validator'; +import { + BucketerParams, + DecisionResponse, + Experiment, + ExperimentBucketMap, + ExperimentCore, + FeatureFlag, + Holdout, + OptimizelyDecideOption, + OptimizelyUserContext, + UserAttributes, + UserProfile, + UserProfileService, + UserProfileServiceAsync, + Variation, +} from '../../shared_types'; + +import { + INVALID_USER_ID, + INVALID_VARIATION_KEY, + NO_VARIATION_FOR_EXPERIMENT_KEY, + USER_NOT_IN_FORCED_VARIATION, + USER_PROFILE_LOOKUP_ERROR, + USER_PROFILE_SAVE_ERROR, + BUCKETING_ID_NOT_STRING, +} from 'error_message'; + +import { + SAVED_USER_VARIATION, + SAVED_VARIATION_NOT_FOUND, + USER_HAS_NO_FORCED_VARIATION, + USER_MAPPED_TO_FORCED_VARIATION, + USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT, + VALID_BUCKETING_ID, + VARIATION_REMOVED_FOR_USER, +} from 'log_message'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { CmabService } from './cmab/cmab_service'; +import { Maybe, OpType, OpValue } from '../../utils/type'; +import { Value } from '../../utils/promise/operation_value'; +import * as featureToggle from '../../feature_toggle'; + +export const EXPERIMENT_NOT_RUNNING = 'Experiment %s is not running.'; +export const RETURNING_STORED_VARIATION = + 'Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile.'; +export const USER_NOT_IN_EXPERIMENT = 'User %s does not meet conditions to be in experiment %s.'; +export const USER_HAS_NO_VARIATION = 'User %s is in no variation of experiment %s.'; +export const USER_HAS_VARIATION = 'User %s is in variation %s of experiment %s.'; +export const USER_FORCED_IN_VARIATION = 'User %s is forced in variation %s.'; +export const FORCED_BUCKETING_FAILED = 'Variation key %s is not in datafile. Not activating user %s.'; +export const EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for %s "%s": %s.'; +export const AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for %s %s collectively evaluated to %s.'; +export const USER_IN_ROLLOUT = 'User %s is in rollout of feature %s.'; +export const USER_NOT_IN_ROLLOUT = 'User %s is not in rollout of feature %s.'; +export const FEATURE_HAS_NO_EXPERIMENTS = 'Feature %s is not attached to any experiments.'; +export const USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE = + 'User %s does not meet conditions for targeting rule %s.'; +export const USER_NOT_BUCKETED_INTO_TARGETING_RULE = +'User %s not bucketed into targeting rule %s due to traffic allocation. Trying everyone rule.'; +export const USER_BUCKETED_INTO_TARGETING_RULE = 'User %s bucketed into targeting rule %s.'; +export const NO_ROLLOUT_EXISTS = 'There is no rollout of feature %s.'; +export const INVALID_ROLLOUT_ID = 'Invalid rollout ID %s attached to feature %s'; +export const ROLLOUT_HAS_NO_EXPERIMENTS = 'Rollout of feature %s has no experiments'; +export const IMPROPERLY_FORMATTED_EXPERIMENT = 'Experiment key %s is improperly formatted.'; +export const USER_HAS_FORCED_VARIATION = + 'Variation %s is mapped to experiment %s and user %s in the forced variation map.'; +export const USER_MEETS_CONDITIONS_FOR_TARGETING_RULE = 'User %s meets conditions for targeting rule %s.'; +export const USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED = + 'Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.'; +export const USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED = + 'Variation (%s) is mapped to flag (%s) and user (%s) in the forced decision map.'; +export const USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID = + 'Invalid variation is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.'; +export const USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED_BUT_INVALID = + 'Invalid variation is mapped to flag (%s) and user (%s) in the forced decision map.'; +export const CMAB_NOT_SUPPORTED_IN_SYNC = 'CMAB is not supported in sync mode.'; +export const CMAB_FETCH_FAILED = 'Failed to fetch CMAB data for experiment %s.'; +export const CMAB_FETCHED_VARIATION_INVALID = 'Fetched variation %s for cmab experiment %s is invalid.'; +export const HOLDOUT_NOT_RUNNING = 'Holdout %s is not running.'; +export const USER_MEETS_CONDITIONS_FOR_HOLDOUT = 'User %s meets conditions for holdout %s.'; +export const USER_DOESNT_MEET_CONDITIONS_FOR_HOLDOUT = 'User %s does not meet conditions for holdout %s.'; +export const USER_BUCKETED_INTO_HOLDOUT_VARIATION = 'User %s is in variation %s of holdout %s.'; +export const USER_NOT_BUCKETED_INTO_HOLDOUT_VARIATION = 'User %s is in no holdout variation.'; + +export interface DecisionObj { + experiment: Experiment | Holdout | null; + variation: Variation | null; + decisionSource: DecisionSource; + cmabUuid?: string; +} + +interface DecisionServiceOptions { + userProfileService?: UserProfileService; + userProfileServiceAsync?: UserProfileServiceAsync; + logger?: LoggerFacade; + UNSTABLE_conditionEvaluators: unknown; + cmabService: CmabService; +} + +interface DeliveryRuleResponse<T, K> extends DecisionResponse<T> { + skipToEveryoneElse: K; +} + +interface UserProfileTracker { + userProfile: ExperimentBucketMap | null; + isProfileUpdated: boolean; +} + +type VarationKeyWithCmabParams = { + variationKey?: string; + cmabUuid?: string; +}; +export type DecisionReason = [string, ...any[]]; +export type VariationResult = DecisionResponse<VarationKeyWithCmabParams>; +export type DecisionResult = DecisionResponse<DecisionObj>; +type VariationIdWithCmabParams = { + variationId? : string; + cmabUuid?: string; +}; +export type DecideOptionsMap = Partial<Record<OptimizelyDecideOption, boolean>>; + +export const CMAB_DUMMY_ENTITY_ID= '$' + +/** + * Optimizely's decision service that determines which variation of an experiment the user will be allocated to. + * + * The decision service contains all logic around how a user decision is made. This includes all of the following (in order): + * 1. Checking experiment status + * 2. Checking forced bucketing + * 3. Checking whitelisting + * 4. Checking user profile service for past bucketing decisions (sticky bucketing) + * 5. Checking audience targeting + * 6. Using Murmurhash3 to bucket the user. + * + * @constructor + * @param {DecisionServiceOptions} options + * @returns {DecisionService} + */ +export class DecisionService { + private logger?: LoggerFacade; + private audienceEvaluator: AudienceEvaluator; + private forcedVariationMap: { [key: string]: { [id: string]: string } }; + private userProfileService?: UserProfileService; + private userProfileServiceAsync?: UserProfileServiceAsync; + private cmabService: CmabService; + + constructor(options: DecisionServiceOptions) { + this.logger = options.logger; + this.audienceEvaluator = createAudienceEvaluator(options.UNSTABLE_conditionEvaluators, this.logger); + this.forcedVariationMap = {}; + this.userProfileService = options.userProfileService; + this.userProfileServiceAsync = options.userProfileServiceAsync; + this.cmabService = options.cmabService; + } + + private isCmab(experiment: Experiment): boolean { + return !!experiment.cmab; + } + + /** + * Resolves the variation into which the visitor will be bucketed. + * + * @param {ProjectConfig} configObj - The parsed project configuration object. + * @param {Experiment} experiment - The experiment for which the variation is being resolved. + * @param {OptimizelyUserContext} user - The user context associated with this decision. + * @returns {DecisionResponse<string|null>} - A DecisionResponse containing the variation the user is bucketed into, + * along with the decision reasons. + */ + private resolveVariation<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext, + decideOptions: DecideOptionsMap, + userProfileTracker?: UserProfileTracker, + ): Value<OP, VariationResult> { + const userId = user.getUserId(); + + const experimentKey = experiment.key; + + if (!isActive(configObj, experimentKey)) { + this.logger?.info(EXPERIMENT_NOT_RUNNING, experimentKey); + return Value.of(op, { + result: {}, + reasons: [[EXPERIMENT_NOT_RUNNING, experimentKey]], + }); + } + + const decideReasons: DecisionReason[] = []; + + const decisionForcedVariation = this.getForcedVariation(configObj, experimentKey, userId); + decideReasons.push(...decisionForcedVariation.reasons); + const forcedVariationKey = decisionForcedVariation.result; + + if (forcedVariationKey) { + return Value.of(op, { + result: { variationKey: forcedVariationKey }, + reasons: decideReasons, + }); + } + + const decisionWhitelistedVariation = this.getWhitelistedVariation(experiment, userId); + decideReasons.push(...decisionWhitelistedVariation.reasons); + let variation = decisionWhitelistedVariation.result; + if (variation) { + return Value.of(op, { + result: { variationKey: variation.key }, + reasons: decideReasons, + }); + } + + // check for sticky bucketing + if (userProfileTracker) { + variation = this.getStoredVariation(configObj, experiment, userId, userProfileTracker.userProfile); + if (variation) { + this.logger?.info( + RETURNING_STORED_VARIATION, + variation.key, + experimentKey, + userId, + ); + decideReasons.push([ + RETURNING_STORED_VARIATION, + variation.key, + experimentKey, + userId, + ]); + return Value.of(op, { + result: { variationKey: variation.key }, + reasons: decideReasons, + }); + } + } + + const decisionifUserIsInAudience = this.checkIfUserIsInAudience( + configObj, + experiment, + AUDIENCE_EVALUATION_TYPES.EXPERIMENT, + user, + '' + ); + decideReasons.push(...decisionifUserIsInAudience.reasons); + if (!decisionifUserIsInAudience.result) { + this.logger?.info( + USER_NOT_IN_EXPERIMENT, + userId, + experimentKey, + ); + decideReasons.push([ + USER_NOT_IN_EXPERIMENT, + userId, + experimentKey, + ]); + return Value.of(op, { + result: {}, + reasons: decideReasons, + }); + } + + const decisionVariationValue = this.isCmab(experiment) ? + this.getDecisionForCmabExperiment(op, configObj, experiment, user, decideOptions) : + this.getDecisionFromBucketer(op, configObj, experiment, user); + + return decisionVariationValue.then((variationResult): Value<OP, VariationResult> => { + decideReasons.push(...variationResult.reasons); + if (variationResult.error) { + return Value.of(op, { + error: true, + result: {}, + reasons: decideReasons, + }); + } + + const variationId = variationResult.result.variationId; + variation = variationId ? configObj.variationIdMap[variationId] : null; + if (!variation) { + this.logger?.debug( + USER_HAS_NO_VARIATION, + userId, + experimentKey, + ); + decideReasons.push([ + USER_HAS_NO_VARIATION, + userId, + experimentKey, + ]); + return Value.of(op, { + result: {}, + reasons: decideReasons, + }); + } + + this.logger?.info( + USER_HAS_VARIATION, + userId, + variation.key, + experimentKey, + ); + decideReasons.push([ + USER_HAS_VARIATION, + userId, + variation.key, + experimentKey, + ]); + // update experiment bucket map if decide options do not include shouldIgnoreUPS + if (userProfileTracker) { + this.updateUserProfile(experiment, variation, userProfileTracker); + } + + return Value.of(op, { + result: { variationKey: variation.key, cmabUuid: variationResult.result.cmabUuid }, + reasons: decideReasons, + }); + }); + } + + private getDecisionForCmabExperiment<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext, + decideOptions: DecideOptionsMap, + ): Value<OP, DecisionResponse<VariationIdWithCmabParams>> { + if (op === 'sync') { + return Value.of(op, { + error: false, // this is not considered an error, the evaluation should continue to next rule + result: {}, + reasons: [[CMAB_NOT_SUPPORTED_IN_SYNC]], + }); + } + + const userId = user.getUserId(); + const attributes = user.getAttributes(); + + const bucketingId = this.getBucketingId(userId, attributes); + const bucketerParams = this.buildBucketerParams(configObj, experiment, bucketingId, userId); + + const bucketerResult = bucket(bucketerParams); + + // this means the user is not in the cmab experiment + if (bucketerResult.result !== CMAB_DUMMY_ENTITY_ID) { + return Value.of(op, { + error: false, + result: {}, + reasons: bucketerResult.reasons, + }); + } + + const cmabPromise = this.cmabService.getDecision(configObj, user, experiment.id, decideOptions).then( + (cmabDecision) => { + return { + error: false, + result: cmabDecision, + reasons: [] as DecisionReason[], + }; + } + ).catch((ex: any) => { + this.logger?.error(CMAB_FETCH_FAILED, experiment.key); + return { + error: true, + result: {}, + reasons: [[CMAB_FETCH_FAILED, experiment.key]] as DecisionReason[], + }; + }); + + return Value.of(op, cmabPromise); + } + + private getDecisionFromBucketer<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext + ): Value<OP, DecisionResponse<VariationIdWithCmabParams>> { + const userId = user.getUserId(); + const attributes = user.getAttributes(); + + // by default, the bucketing ID should be the user ID + const bucketingId = this.getBucketingId(userId, attributes); + const bucketerParams = this.buildBucketerParams(configObj, experiment, bucketingId, userId); + + const decisionVariation = bucket(bucketerParams); + return Value.of(op, { + result: { + variationId: decisionVariation.result || undefined, + }, + reasons: decisionVariation.reasons, + }); + } + + /** + * Gets variation where visitor will be bucketed. + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {Experiment} experiment + * @param {OptimizelyUserContext} user A user context + * @param {[key: string]: boolean} options Optional map of decide options + * @return {DecisionResponse<string|null>} DecisionResponse containing the variation the user is bucketed into + * and the decide reasons. + */ + getVariation( + configObj: ProjectConfig, + experiment: Experiment, + user: OptimizelyUserContext, + options: DecideOptionsMap = {} + ): DecisionResponse<string | null> { + const shouldIgnoreUPS = options[OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE]; + const userProfileTracker: Maybe<UserProfileTracker> = shouldIgnoreUPS ? undefined + : { + isProfileUpdated: false, + userProfile: this.resolveExperimentBucketMap('sync', user.getUserId(), user.getAttributes()).get(), + }; + + const result = this.resolveVariation('sync', configObj, experiment, user, options, userProfileTracker).get(); + + if(userProfileTracker) { + this.saveUserProfile('sync', user.getUserId(), userProfileTracker) + } + + return { + result: result.result.variationKey || null, + reasons: result.reasons, + } + } + + /** + * Merges attributes from attributes[STICKY_BUCKETING_KEY] and userProfileService + * @param {string} userId + * @param {UserAttributes} attributes + * @return {ExperimentBucketMap} finalized copy of experiment_bucket_map + */ + private resolveExperimentBucketMap<OP extends OpType>( + op: OP, + userId: string, + attributes: UserAttributes = {}, + ): Value<OP, ExperimentBucketMap> { + const fromAttributes = (attributes[CONTROL_ATTRIBUTES.STICKY_BUCKETING_KEY] || {}) as any as ExperimentBucketMap; + return this.getUserProfile(op, userId).then((userProfile) => { + const fromUserProfileService = userProfile?.experiment_bucket_map || {}; + return Value.of(op, { + ...fromUserProfileService, + ...fromAttributes, + }); + }); + } + + /** + * Checks if user is whitelisted into any variation and return that variation if so + * @param {Experiment} experiment + * @param {string} userId + * @return {DecisionResponse<Variation|null>} DecisionResponse containing the forced variation if it exists + * or user ID and the decide reasons. + */ + private getWhitelistedVariation( + experiment: Experiment, + userId: string + ): DecisionResponse<Variation | null> { + const decideReasons: DecisionReason[] = []; + if (experiment.forcedVariations && experiment.forcedVariations.hasOwnProperty(userId)) { + const forcedVariationKey = experiment.forcedVariations[userId]; + if (experiment.variationKeyMap.hasOwnProperty(forcedVariationKey)) { + this.logger?.info( + USER_FORCED_IN_VARIATION, + userId, + forcedVariationKey, + ); + decideReasons.push([ + USER_FORCED_IN_VARIATION, + userId, + forcedVariationKey, + ]); + return { + result: experiment.variationKeyMap[forcedVariationKey], + reasons: decideReasons, + }; + } else { + this.logger?.error( + FORCED_BUCKETING_FAILED, + forcedVariationKey, + userId, + ); + decideReasons.push([ + FORCED_BUCKETING_FAILED, + forcedVariationKey, + userId, + ]); + return { + result: null, + reasons: decideReasons, + }; + } + } + + return { + result: null, + reasons: decideReasons, + }; + } + + /** + * Checks whether the user is included in experiment audience + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {string} experimentKey Key of experiment being validated + * @param {string} evaluationAttribute String representing experiment key or rule + * @param {string} userId ID of user + * @param {UserAttributes} attributes Optional parameter for user's attributes + * @param {string} loggingKey String representing experiment key or rollout rule. To be used in log messages only. + * @return {DecisionResponse<boolean>} DecisionResponse DecisionResponse containing result true if user meets audience conditions and + * the decide reasons. + */ + private checkIfUserIsInAudience( + configObj: ProjectConfig, + experiment: ExperimentCore, + evaluationAttribute: string, + user: OptimizelyUserContext, + loggingKey?: string | number, + ): DecisionResponse<boolean> { + const decideReasons: DecisionReason[] = []; + const experimentAudienceConditions = experiment.audienceConditions || experiment.audienceIds; + const audiencesById = getAudiencesById(configObj); + + this.logger?.debug( + EVALUATING_AUDIENCES_COMBINED, + evaluationAttribute, + loggingKey || experiment.key, + JSON.stringify(experimentAudienceConditions), + ); + decideReasons.push([ + EVALUATING_AUDIENCES_COMBINED, + evaluationAttribute, + loggingKey || experiment.key, + JSON.stringify(experimentAudienceConditions), + ]); + + const result = this.audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, user); + + this.logger?.info( + AUDIENCE_EVALUATION_RESULT_COMBINED, + evaluationAttribute, + loggingKey || experiment.key, + result.toString().toUpperCase(), + ); + decideReasons.push([ + AUDIENCE_EVALUATION_RESULT_COMBINED, + evaluationAttribute, + loggingKey || experiment.key, + result.toString().toUpperCase(), + ]); + + return { + result: result, + reasons: decideReasons, + }; + } + + /** + * Given an experiment key and user ID, returns params used in bucketer call + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {string} experimentKey Experiment key used for bucketer + * @param {string} bucketingId ID to bucket user into + * @param {string} userId ID of user to be bucketed + * @return {BucketerParams} + */ + private buildBucketerParams( + configObj: ProjectConfig, + experiment: Experiment | Holdout, + bucketingId: string, + userId: string + ): BucketerParams { + let validateEntity = true; + + let trafficAllocationConfig = experiment.trafficAllocation; + + if ('cmab' in experiment && experiment.cmab) { + trafficAllocationConfig = [{ + entityId: CMAB_DUMMY_ENTITY_ID, + endOfRange: experiment.cmab.trafficAllocation + }]; + + validateEntity = false; + } + + return { + bucketingId, + experimentId: experiment.id, + experimentKey: experiment.key, + experimentIdMap: configObj.experimentIdMap, + experimentKeyMap: configObj.experimentKeyMap, + groupIdMap: configObj.groupIdMap, + logger: this.logger, + trafficAllocationConfig, + userId, + variationIdMap: configObj.variationIdMap, + validateEntity, + } + } + + /** + * Determines if a user should be bucketed into a holdout variation. + * @param {ProjectConfig} configObj - The parsed project configuration object. + * @param {Holdout} holdout - The holdout to evaluate. + * @param {OptimizelyUserContext} user - The user context. + * @returns {DecisionResponse<DecisionObj>} - DecisionResponse containing holdout decision and reasons. + */ + private getVariationForHoldout( + configObj: ProjectConfig, + holdout: Holdout, + user: OptimizelyUserContext, + ): DecisionResponse<DecisionObj> { + const userId = user.getUserId(); + const decideReasons: DecisionReason[] = []; + + if (holdout.status !== 'Running') { + const reason: DecisionReason = [HOLDOUT_NOT_RUNNING, holdout.key]; + decideReasons.push(reason); + this.logger?.info(HOLDOUT_NOT_RUNNING, holdout.key); + return { + result: { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.HOLDOUT + }, + reasons: decideReasons + }; + } + + const audienceResult = this.checkIfUserIsInAudience( + configObj, + holdout, + AUDIENCE_EVALUATION_TYPES.EXPERIMENT, + user + ); + decideReasons.push(...audienceResult.reasons); + + if (!audienceResult.result) { + const reason: DecisionReason = [USER_DOESNT_MEET_CONDITIONS_FOR_HOLDOUT, userId, holdout.key]; + decideReasons.push(reason); + this.logger?.info(USER_DOESNT_MEET_CONDITIONS_FOR_HOLDOUT, userId, holdout.key); + return { + result: { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.HOLDOUT + }, + reasons: decideReasons + }; + } + + const reason: DecisionReason = [USER_MEETS_CONDITIONS_FOR_HOLDOUT, userId, holdout.key]; + decideReasons.push(reason); + this.logger?.info(USER_MEETS_CONDITIONS_FOR_HOLDOUT, userId, holdout.key); + + const attributes = user.getAttributes(); + const bucketingId = this.getBucketingId(userId, attributes); + const bucketerParams = this.buildBucketerParams(configObj, holdout, bucketingId, userId); + const bucketResult = bucket(bucketerParams); + + decideReasons.push(...bucketResult.reasons); + + if (bucketResult.result) { + const variation = configObj.variationIdMap[bucketResult.result]; + if (variation) { + const bucketReason: DecisionReason = [USER_BUCKETED_INTO_HOLDOUT_VARIATION, userId, holdout.key, variation.key]; + decideReasons.push(bucketReason); + this.logger?.info(USER_BUCKETED_INTO_HOLDOUT_VARIATION, userId, holdout.key, variation.key); + + return { + result: { + experiment: holdout, + variation: variation, + decisionSource: DECISION_SOURCES.HOLDOUT + }, + reasons: decideReasons + }; + } + } + + const noBucketReason: DecisionReason = [USER_NOT_BUCKETED_INTO_HOLDOUT_VARIATION, userId]; + decideReasons.push(noBucketReason); + this.logger?.info(USER_NOT_BUCKETED_INTO_HOLDOUT_VARIATION, userId); + return { + result: { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.HOLDOUT + }, + reasons: decideReasons + }; + } + + /** + * Pull the stored variation out of the experimentBucketMap for an experiment/userId + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {Experiment} experiment + * @param {string} userId + * @param {ExperimentBucketMap} experimentBucketMap mapping experiment => { variation_id: <variationId> } + * @return {Variation|null} the stored variation or null if the user profile does not have one for the given experiment + */ + private getStoredVariation( + configObj: ProjectConfig, + experiment: Experiment, + userId: string, + experimentBucketMap: ExperimentBucketMap | null + ): Variation | null { + if (experimentBucketMap?.hasOwnProperty(experiment.id)) { + const decision = experimentBucketMap[experiment.id]; + const variationId = decision.variation_id; + if (configObj.variationIdMap.hasOwnProperty(variationId)) { + return configObj.variationIdMap[decision.variation_id]; + } else { + this.logger?.info( + SAVED_VARIATION_NOT_FOUND, + userId, + variationId, + experiment.key, + ); + } + } + + return null; + } + + /** + * Get the user profile with the given user ID + * @param {string} userId + * @return {UserProfile} the stored user profile or an empty profile if one isn't found or error + */ + private getUserProfile<OP extends OpType>(op: OP, userId: string): Value<OP, UserProfile> { + const emptyProfile = { + user_id: userId, + experiment_bucket_map: {}, + }; + + if (this.userProfileService) { + try { + return Value.of(op, this.userProfileService.lookup(userId)); + } catch (ex: any) { + this.logger?.error( + USER_PROFILE_LOOKUP_ERROR, + userId, + ex.message, + ); + } + return Value.of(op, emptyProfile); + } + + if (this.userProfileServiceAsync && op === 'async') { + return Value.of(op, this.userProfileServiceAsync.lookup(userId).catch((ex: any) => { + this.logger?.error( + USER_PROFILE_LOOKUP_ERROR, + userId, + ex.message, + ); + return emptyProfile; + })); + } + + return Value.of(op, emptyProfile); + } + + private updateUserProfile( + experiment: Experiment, + variation: Variation, + userProfileTracker: UserProfileTracker + ): void { + if(!userProfileTracker.userProfile) { + return + } + + userProfileTracker.userProfile[experiment.id] = { + variation_id: variation.id + } + userProfileTracker.isProfileUpdated = true + } + + /** + * Saves the bucketing decision to the user profile + * @param {Experiment} experiment + * @param {Variation} variation + * @param {string} userId + * @param {ExperimentBucketMap} experimentBucketMap + */ + private saveUserProfile<OP extends OpType>( + op: OP, + userId: string, + userProfileTracker: UserProfileTracker + ): Value<OP, unknown> { + const { userProfile, isProfileUpdated } = userProfileTracker; + + if (!userProfile || !isProfileUpdated) { + return Value.of(op, undefined); + } + + if (op === 'sync' && !this.userProfileService) { + return Value.of(op, undefined); + } + + if (this.userProfileService) { + try { + this.userProfileService.save({ + user_id: userId, + experiment_bucket_map: userProfile, + }); + + this.logger?.info( + SAVED_USER_VARIATION, + userId, + ); + } catch (ex: any) { + this.logger?.error(USER_PROFILE_SAVE_ERROR, userId, ex.message); + } + return Value.of(op, undefined); + } + + if (this.userProfileServiceAsync) { + return Value.of(op, this.userProfileServiceAsync.save({ + user_id: userId, + experiment_bucket_map: userProfile, + }).catch((ex: any) => { + this.logger?.error(USER_PROFILE_SAVE_ERROR, userId, ex.message); + })); + } + + return Value.of(op, undefined); + } + + + /** + * Determines variations for the specified feature flags. + * + * @param {ProjectConfig} configObj - The parsed project configuration object. + * @param {FeatureFlag[]} featureFlags - The feature flags for which variations are to be determined. + * @param {OptimizelyUserContext} user - The user context associated with this decision. + * @param {Record<string, boolean>} options - An optional map of decision options. + * @returns {DecisionResponse<DecisionObj>[]} - An array of DecisionResponse containing objects with + * experiment, variation, decisionSource properties, and decision reasons. + */ + getVariationsForFeatureList( + configObj: ProjectConfig, + featureFlags: FeatureFlag[], + user: OptimizelyUserContext, + options: DecideOptionsMap = {}): DecisionResult[] { + return this.resolveVariationsForFeatureList('sync', configObj, featureFlags, user, options).get(); + } + + resolveVariationsForFeatureList<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + featureFlags: FeatureFlag[], + user: OptimizelyUserContext, + options: DecideOptionsMap): Value<OP, DecisionResult[]> { + const userId = user.getUserId(); + const attributes = user.getAttributes(); + const decisions: DecisionResponse<DecisionObj>[] = []; + // const userProfileTracker : UserProfileTracker = { + // isProfileUpdated: false, + // userProfile: null, + // } + const shouldIgnoreUPS = !!options[OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE]; + + const userProfileTrackerValue: Value<OP, Maybe<UserProfileTracker>> = shouldIgnoreUPS ? Value.of(op, undefined) + : this.resolveExperimentBucketMap(op, userId, attributes).then((userProfile) => { + return Value.of(op, { + isProfileUpdated: false, + userProfile: userProfile, + }); + }); + + return userProfileTrackerValue.then((userProfileTracker) => { + const flagResults = featureFlags.map((feature) => this.resolveVariationForFlag(op, configObj, feature, user, options, userProfileTracker)); + const opFlagResults = Value.all(op, flagResults); + + return opFlagResults.then(() => { + if(userProfileTracker) { + this.saveUserProfile(op, userId, userProfileTracker); + } + return opFlagResults; + }); + }); + } + + private resolveVariationForFlag<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + feature: FeatureFlag, + user: OptimizelyUserContext, + decideOptions: DecideOptionsMap, + userProfileTracker?: UserProfileTracker + ): Value<OP, DecisionResult> { + const decideReasons: DecisionReason[] = []; + + const forcedDecisionResponse = this.findValidatedForcedDecision(configObj, user, feature.key); + decideReasons.push(...forcedDecisionResponse.reasons); + + if (forcedDecisionResponse.result) { + return Value.of(op, { + result: { + variation: forcedDecisionResponse.result, + experiment: null, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: decideReasons, + }); + } + if (featureToggle.holdout()) { + const holdouts = getHoldoutsForFlag(configObj, feature.key); + + for (const holdout of holdouts) { + const holdoutDecision = this.getVariationForHoldout(configObj, holdout, user); + decideReasons.push(...holdoutDecision.reasons); + + if (holdoutDecision.result.variation) { + return Value.of(op, { + result: holdoutDecision.result, + reasons: decideReasons, + }); + } + } + } + + return this.getVariationForFeatureExperiment(op, configObj, feature, user, decideOptions, userProfileTracker).then((experimentDecision) => { + if (experimentDecision.error || experimentDecision.result.variation !== null) { + return Value.of(op, { + ...experimentDecision, + reasons: [...decideReasons, ...experimentDecision.reasons], + }); + } + + decideReasons.push(...experimentDecision.reasons); + + const rolloutDecision = this.getVariationForRollout(configObj, feature, user); + decideReasons.push(...rolloutDecision.reasons); + const rolloutDecisionResult = rolloutDecision.result; + const userId = user.getUserId(); + + if (rolloutDecisionResult.variation) { + this.logger?.debug(USER_IN_ROLLOUT, userId, feature.key); + decideReasons.push([USER_IN_ROLLOUT, userId, feature.key]); + } else { + this.logger?.debug(USER_NOT_IN_ROLLOUT, userId, feature.key); + decideReasons.push([USER_NOT_IN_ROLLOUT, userId, feature.key]); + } + + return Value.of(op, { + result: rolloutDecisionResult, + reasons: decideReasons, + }); + }); + } + + /** + * Given a feature, user ID, and attributes, returns a decision response containing + * an object representing a decision and decide reasons. If the user was bucketed into + * a variation for the given feature and attributes, the decision object will have variation and + * experiment properties (both objects), as well as a decisionSource property. + * decisionSource indicates whether the decision was due to a rollout or an + * experiment. + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {FeatureFlag} feature A feature flag object from project configuration + * @param {OptimizelyUserContext} user A user context + * @param {[key: string]: boolean} options Map of decide options + * @return {DecisionResponse} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource + * properties and decide reasons. If the user was not bucketed into a variation, the variation + * property in decision object is null. + */ + getVariationForFeature( + configObj: ProjectConfig, + feature: FeatureFlag, + user: OptimizelyUserContext, + options: DecideOptionsMap = {} + ): DecisionResponse<DecisionObj> { + return this.resolveVariationsForFeatureList('sync', configObj, [feature], user, options).get()[0] + } + + private getVariationForFeatureExperiment<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + feature: FeatureFlag, + user: OptimizelyUserContext, + decideOptions: DecideOptionsMap, + userProfileTracker?: UserProfileTracker, + ): Value<OP, DecisionResult> { + + // const decideReasons: DecisionReason[] = []; + // let variationKey = null; + // let decisionVariation; + // let index; + // let variationForFeatureExperiment; + + if (feature.experimentIds.length === 0) { + this.logger?.debug(FEATURE_HAS_NO_EXPERIMENTS, feature.key); + return Value.of(op, { + result: { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: [ + [FEATURE_HAS_NO_EXPERIMENTS, feature.key], + ], + }); + } + + return this.traverseFeatureExperimentList(op, configObj, feature, 0, user, [], decideOptions, userProfileTracker); + } + + private traverseFeatureExperimentList<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + feature: FeatureFlag, + fromIndex: number, + user: OptimizelyUserContext, + decideReasons: DecisionReason[], + decideOptions: DecideOptionsMap, + userProfileTracker?: UserProfileTracker, + ): Value<OP, DecisionResult> { + const experimentIds = feature.experimentIds; + if (fromIndex >= experimentIds.length) { + return Value.of(op, { + result: { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: decideReasons, + }); + } + + const experiment = getExperimentFromId(configObj, experimentIds[fromIndex], this.logger); + if (!experiment) { + return this.traverseFeatureExperimentList( + op, configObj, feature, fromIndex + 1, user, decideReasons, decideOptions, userProfileTracker); + } + + const decisionVariationValue = this.getVariationFromExperimentRule( + op, configObj, feature.key, experiment, user, decideOptions, userProfileTracker, + ); + + return decisionVariationValue.then((decisionVariation) => { + decideReasons.push(...decisionVariation.reasons); + + if (decisionVariation.error) { + return Value.of(op, { + error: true, + result: { + experiment, + variation: null, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: decideReasons, + }); + } + + if(!decisionVariation.result.variationKey) { + return this.traverseFeatureExperimentList( + op, configObj, feature, fromIndex + 1, user, decideReasons, decideOptions, userProfileTracker); + } + + const variationKey = decisionVariation.result.variationKey; + let variation: Variation | null = experiment.variationKeyMap[variationKey]; + if (!variation) { + variation = getFlagVariationByKey(configObj, feature.key, variationKey); + } + + return Value.of(op, { + result: { + cmabUuid: decisionVariation.result.cmabUuid, + experiment, + variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: decideReasons, + }); + }); + } + + private getVariationForRollout( + configObj: ProjectConfig, + feature: FeatureFlag, + user: OptimizelyUserContext, + ): DecisionResponse<DecisionObj> { + const decideReasons: DecisionReason[] = []; + let decisionObj: DecisionObj; + if (!feature.rolloutId) { + this.logger?.debug(NO_ROLLOUT_EXISTS, feature.key); + decideReasons.push([NO_ROLLOUT_EXISTS, feature.key]); + decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + + return { + result: decisionObj, + reasons: decideReasons, + }; + } + + const rollout = configObj.rolloutIdMap[feature.rolloutId]; + if (!rollout) { + this.logger?.error( + INVALID_ROLLOUT_ID, + feature.rolloutId, + feature.key, + ); + decideReasons.push([INVALID_ROLLOUT_ID, feature.rolloutId, feature.key]); + decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + return { + result: decisionObj, + reasons: decideReasons, + }; + } + + const rolloutRules = rollout.experiments; + if (rolloutRules.length === 0) { + this.logger?.error( + ROLLOUT_HAS_NO_EXPERIMENTS, + feature.rolloutId, + ); + decideReasons.push([ROLLOUT_HAS_NO_EXPERIMENTS, feature.rolloutId]); + decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + return { + result: decisionObj, + reasons: decideReasons, + }; + } + let decisionVariation; + let skipToEveryoneElse; + let variation; + let rolloutRule; + let index = 0; + while (index < rolloutRules.length) { + decisionVariation = this.getVariationFromDeliveryRule(configObj, feature.key, rolloutRules, index, user); + decideReasons.push(...decisionVariation.reasons); + variation = decisionVariation.result; + skipToEveryoneElse = decisionVariation.skipToEveryoneElse; + if (variation) { + rolloutRule = configObj.experimentIdMap[rolloutRules[index].id]; + decisionObj = { + experiment: rolloutRule, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT + }; + return { + result: decisionObj, + reasons: decideReasons, + }; + } + // the last rule is special for "Everyone Else" + index = skipToEveryoneElse ? (rolloutRules.length - 1) : (index + 1); + } + + decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + + return { + result: decisionObj, + reasons: decideReasons, + }; + } + + /** + * Get bucketing Id from user attributes. + * @param {string} userId + * @param {UserAttributes} attributes + * @returns {string} Bucketing Id if it is a string type in attributes, user Id otherwise. + */ + private getBucketingId(userId: string, attributes?: UserAttributes): string { + let bucketingId = userId; + + // If the bucketing ID key is defined in attributes, than use that in place of the userID for the murmur hash key + if ( + attributes != null && + typeof attributes === 'object' && + attributes.hasOwnProperty(CONTROL_ATTRIBUTES.BUCKETING_ID) + ) { + if (typeof attributes[CONTROL_ATTRIBUTES.BUCKETING_ID] === 'string') { + bucketingId = String(attributes[CONTROL_ATTRIBUTES.BUCKETING_ID]); + this.logger?.debug(VALID_BUCKETING_ID, bucketingId); + } else { + this.logger?.warn(BUCKETING_ID_NOT_STRING); + } + } + + return bucketingId; + } + + /** + * Finds a validated forced decision for specific flagKey and optional ruleKey. + * @param {ProjectConfig} config A projectConfig. + * @param {OptimizelyUserContext} user A Optimizely User Context. + * @param {string} flagKey A flagKey. + * @param {ruleKey} ruleKey A ruleKey (optional). + * @return {DecisionResponse<Variation|null>} DecisionResponse object containing valid variation object and decide reasons. + */ + findValidatedForcedDecision( + config: ProjectConfig, + user: OptimizelyUserContext, + flagKey: string, + ruleKey?: string + ): DecisionResponse<Variation | null> { + + const decideReasons: DecisionReason[] = []; + const forcedDecision = user.getForcedDecision({ flagKey, ruleKey }); + let variation = null; + let variationKey; + const userId = user.getUserId() + if (config && forcedDecision) { + variationKey = forcedDecision.variationKey; + variation = getFlagVariationByKey(config, flagKey, variationKey); + if (variation) { + if (ruleKey) { + this.logger?.info( + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED, + variationKey, + flagKey, + ruleKey, + userId + ); + decideReasons.push([ + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED, + variationKey, + flagKey, + ruleKey, + userId + ]); + } else { + this.logger?.info( + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, + variationKey, + flagKey, + userId + ); + decideReasons.push([ + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, + variationKey, + flagKey, + userId + ]) + } + } else { + if (ruleKey) { + this.logger?.info( + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID, + flagKey, + ruleKey, + userId + ); + decideReasons.push([ + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID, + flagKey, + ruleKey, + userId + ]); + } else { + this.logger?.info( + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED_BUT_INVALID, + flagKey, + userId + ); + decideReasons.push([ + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED_BUT_INVALID, + flagKey, + userId + ]) + } + } + } + + return { + result: variation, + reasons: decideReasons, + } + } + + /** + * Removes forced variation for given userId and experimentKey + * @param {string} userId String representing the user id + * @param {string} experimentId Number representing the experiment id + * @param {string} experimentKey Key representing the experiment id + * @throws If the user id is not valid or not in the forced variation map + */ + private removeForcedVariation(userId: string, experimentId: string, experimentKey: string): void { + if (!userId) { + throw new OptimizelyError(INVALID_USER_ID); + } + + if (this.forcedVariationMap.hasOwnProperty(userId)) { + delete this.forcedVariationMap[userId][experimentId]; + this.logger?.debug( + VARIATION_REMOVED_FOR_USER, + experimentKey, + userId, + ); + } else { + throw new OptimizelyError(USER_NOT_IN_FORCED_VARIATION, userId); + } + } + + /** + * Sets forced variation for given userId and experimentKey + * @param {string} userId String representing the user id + * @param {string} experimentId Number representing the experiment id + * @param {number} variationId Number representing the variation id + * @throws If the user id is not valid + */ + private setInForcedVariationMap(userId: string, experimentId: string, variationId: string): void { + if (this.forcedVariationMap.hasOwnProperty(userId)) { + this.forcedVariationMap[userId][experimentId] = variationId; + } else { + this.forcedVariationMap[userId] = {}; + this.forcedVariationMap[userId][experimentId] = variationId; + } + + this.logger?.debug( + USER_MAPPED_TO_FORCED_VARIATION, + variationId, + experimentId, + userId, + ); + } + + /** + * Gets the forced variation key for the given user and experiment. + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Key for experiment. + * @param {string} userId The user Id. + * @return {DecisionResponse<string|null>} DecisionResponse containing variation which the given user and experiment + * should be forced into and the decide reasons. + */ + getForcedVariation( + configObj: ProjectConfig, + experimentKey: string, + userId: string + ): DecisionResponse<string | null> { + const decideReasons: DecisionReason[] = []; + const experimentToVariationMap = this.forcedVariationMap[userId]; + if (!experimentToVariationMap) { + this.logger?.debug( + USER_HAS_NO_FORCED_VARIATION, + userId, + ); + + return { + result: null, + reasons: decideReasons, + }; + } + + let experimentId; + try { + const experiment = getExperimentFromKey(configObj, experimentKey); + if (experiment.hasOwnProperty('id')) { + experimentId = experiment['id']; + } else { + // catching improperly formatted experiments + this.logger?.error( + IMPROPERLY_FORMATTED_EXPERIMENT, + experimentKey, + ); + decideReasons.push([ + IMPROPERLY_FORMATTED_EXPERIMENT, + experimentKey, + ]); + + return { + result: null, + reasons: decideReasons, + }; + } + } catch (ex: any) { + // catching experiment not in datafile + this.logger?.error(ex); + decideReasons.push(ex.message); + + return { + result: null, + reasons: decideReasons, + }; + } + + const variationId = experimentToVariationMap[experimentId]; + if (!variationId) { + this.logger?.debug( + USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT, + experimentKey, + userId, + ); + return { + result: null, + reasons: decideReasons, + }; + } + + const variationKey = getVariationKeyFromId(configObj, variationId); + if (variationKey) { + this.logger?.debug( + USER_HAS_FORCED_VARIATION, + variationKey, + experimentKey, + userId, + ); + decideReasons.push([ + USER_HAS_FORCED_VARIATION, + variationKey, + experimentKey, + userId, + ]); + } else { + this.logger?.debug( + USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT, + experimentKey, + userId, + ); + } + + return { + result: variationKey, + reasons: decideReasons, + }; + } + + /** + * Sets the forced variation for a user in a given experiment + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Key for experiment. + * @param {string} userId The user Id. + * @param {string|null} variationKey Key for variation. If null, then clear the existing experiment-to-variation mapping + * @return {boolean} A boolean value that indicates if the set completed successfully. + */ + setForcedVariation( + configObj: ProjectConfig, + experimentKey: string, + userId: string, + variationKey: string | null + ): boolean { + if (variationKey != null && !stringValidator.validate(variationKey)) { + this.logger?.error(INVALID_VARIATION_KEY); + return false; + } + + let experimentId; + try { + const experiment = getExperimentFromKey(configObj, experimentKey); + if (experiment.hasOwnProperty('id')) { + experimentId = experiment['id']; + } else { + // catching improperly formatted experiments + this.logger?.error( + IMPROPERLY_FORMATTED_EXPERIMENT, + experimentKey, + ); + return false; + } + } catch (ex: any) { + // catching experiment not in datafile + this.logger?.error(ex); + return false; + } + + if (variationKey == null) { + try { + this.removeForcedVariation(userId, experimentId, experimentKey); + return true; + } catch (ex: any) { + this.logger?.error(ex); + return false; + } + } + + const variationId = getVariationIdFromExperimentAndVariationKey(configObj, experimentKey, variationKey); + + if (!variationId) { + this.logger?.error( + NO_VARIATION_FOR_EXPERIMENT_KEY, + variationKey, + experimentKey, + ); + return false; + } + + try { + this.setInForcedVariationMap(userId, experimentId, variationId); + return true; + } catch (ex: any) { + this.logger?.error(ex); + return false; + } + } + + private getVariationFromExperimentRule<OP extends OpType>( + op: OP, + configObj: ProjectConfig, + flagKey: string, + rule: Experiment, + user: OptimizelyUserContext, + decideOptions: DecideOptionsMap, + userProfileTracker?: UserProfileTracker, + ): Value<OP, VariationResult> { + const decideReasons: DecisionReason[] = []; + + // check forced decision first + const forcedDecisionResponse = this.findValidatedForcedDecision(configObj, user, flagKey, rule.key); + decideReasons.push(...forcedDecisionResponse.reasons); + + const forcedVariation = forcedDecisionResponse.result; + if (forcedVariation) { + return Value.of(op, { + result: { variationKey: forcedVariation.key }, + reasons: decideReasons, + }); + } + const decisionVariationValue = this.resolveVariation(op, configObj, rule, user, decideOptions, userProfileTracker); + + return decisionVariationValue.then((variationResult) => { + decideReasons.push(...variationResult.reasons); + return Value.of(op, { + error: variationResult.error, + result: variationResult.result, + reasons: decideReasons, + }); + }); + + // return response; + + // decideReasons.push(...decisionVariation.reasons); + // const variationKey = decisionVariation.result; + + // return { + // result: variationKey, + // reasons: decideReasons, + // }; + } + + private getVariationFromDeliveryRule( + configObj: ProjectConfig, + flagKey: string, + rules: Experiment[], + ruleIndex: number, + user: OptimizelyUserContext + ): DeliveryRuleResponse<Variation | null, boolean> { + const decideReasons: DecisionReason[] = []; + let skipToEveryoneElse = false; + + // check forced decision first + const rule = rules[ruleIndex]; + const forcedDecisionResponse = this.findValidatedForcedDecision(configObj, user, flagKey, rule.key); + decideReasons.push(...forcedDecisionResponse.reasons); + + const forcedVariation = forcedDecisionResponse.result; + if (forcedVariation) { + return { + result: forcedVariation, + reasons: decideReasons, + skipToEveryoneElse, + }; + } + + const userId = user.getUserId(); + const attributes = user.getAttributes(); + const bucketingId = this.getBucketingId(userId, attributes); + const everyoneElse = ruleIndex === rules.length - 1; + const loggingKey = everyoneElse ? "Everyone Else" : ruleIndex + 1; + + let bucketedVariation = null; + let bucketerVariationId; + let bucketerParams; + let decisionVariation; + const decisionifUserIsInAudience = this.checkIfUserIsInAudience( + configObj, + rule, + AUDIENCE_EVALUATION_TYPES.RULE, + user, + loggingKey + ); + decideReasons.push(...decisionifUserIsInAudience.reasons); + if (decisionifUserIsInAudience.result) { + this.logger?.debug( + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, + userId, + loggingKey + ); + decideReasons.push([ + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, + userId, + loggingKey + ]); + + bucketerParams = this.buildBucketerParams(configObj, rule, bucketingId, userId); + decisionVariation = bucket(bucketerParams); + decideReasons.push(...decisionVariation.reasons); + bucketerVariationId = decisionVariation.result; + if (bucketerVariationId) { + bucketedVariation = getVariationFromId(configObj, bucketerVariationId); + } + if (bucketedVariation) { + this.logger?.debug( + USER_BUCKETED_INTO_TARGETING_RULE, + userId, + loggingKey + ); + decideReasons.push([ + USER_BUCKETED_INTO_TARGETING_RULE, + userId, + loggingKey]); + } else if (!everyoneElse) { + // skip this logging for EveryoneElse since this has a message not for EveryoneElse + this.logger?.debug( + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + userId, + loggingKey + ); + decideReasons.push([ + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + userId, + loggingKey + ]); + + // skip the rest of rollout rules to the everyone-else rule if audience matches but not bucketed + skipToEveryoneElse = true; + } + } else { + this.logger?.debug( + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + userId, + loggingKey + ); + decideReasons.push([ + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + userId, + loggingKey + ]); + } + + return { + result: bucketedVariation, + reasons: decideReasons, + skipToEveryoneElse, + }; + } +} + +/** + * Creates an instance of the DecisionService. + * @param {DecisionServiceOptions} options Configuration options + * @return {Object} An instance of the DecisionService + */ +export function createDecisionService(options: DecisionServiceOptions): DecisionService { + return new DecisionService(options); +} diff --git a/lib/entrypoint.test-d.ts b/lib/entrypoint.test-d.ts new file mode 100644 index 000000000..366889ea8 --- /dev/null +++ b/lib/entrypoint.test-d.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expectTypeOf } from 'vitest'; + +import * as browser from './index.browser'; +import * as node from './index.node'; +import * as reactNative from './index.react_native'; + +type WithoutReadonly<T> = { -readonly [P in keyof T]: T[P] }; + +const nodeEntrypoint: WithoutReadonly<typeof node> = node; +const browserEntrypoint: WithoutReadonly<typeof browser> = browser; +const reactNativeEntrypoint: WithoutReadonly<typeof reactNative> = reactNative; + +import { + Config, + Client, + StaticConfigManagerConfig, + OpaqueConfigManager, + PollingConfigManagerConfig, + EventDispatcher, + OpaqueEventProcessor, + BatchEventProcessorOptions, + OdpManagerOptions, + OpaqueOdpManager, + VuidManagerOptions, + OpaqueVuidManager, + OpaqueLevelPreset, + LoggerConfig, + OpaqueLogger, + ErrorHandler, + OpaqueErrorNotifier, +} from './export_types'; + +import { + DECISION_SOURCES, +} from './utils/enums'; + +import { NOTIFICATION_TYPES, DECISION_NOTIFICATION_TYPES } from './notification_center/type'; + +import { LogLevel } from './logging/logger'; + +import { OptimizelyDecideOption } from './shared_types'; +import { Maybe } from './utils/type'; + +export type Entrypoint = { + // client factory + createInstance: (config: Config) => Client; + + // config manager related exports + createStaticProjectConfigManager: (config: StaticConfigManagerConfig) => OpaqueConfigManager; + createPollingProjectConfigManager: (config: PollingConfigManagerConfig) => OpaqueConfigManager; + + // event processor related exports + eventDispatcher: EventDispatcher; + getSendBeaconEventDispatcher: () => Maybe<EventDispatcher>; + createForwardingEventProcessor: (eventDispatcher?: EventDispatcher) => OpaqueEventProcessor; + createBatchEventProcessor: (options?: BatchEventProcessorOptions) => OpaqueEventProcessor; + + // odp manager related exports + createOdpManager: (options?: OdpManagerOptions) => OpaqueOdpManager; + + // vuid manager related exports + createVuidManager: (options?: VuidManagerOptions) => OpaqueVuidManager; + + // logger related exports + LogLevel: typeof LogLevel; + DEBUG: OpaqueLevelPreset, + INFO: OpaqueLevelPreset, + WARN: OpaqueLevelPreset, + ERROR: OpaqueLevelPreset, + createLogger: (config: LoggerConfig) => OpaqueLogger; + + // error related exports + createErrorNotifier: (errorHandler: ErrorHandler) => OpaqueErrorNotifier; + + // enums + DECISION_SOURCES: typeof DECISION_SOURCES; + NOTIFICATION_TYPES: typeof NOTIFICATION_TYPES; + DECISION_NOTIFICATION_TYPES: typeof DECISION_NOTIFICATION_TYPES; + + // decide options + OptimizelyDecideOption: typeof OptimizelyDecideOption; + + // client engine + clientEngine: string; +} + + +expectTypeOf(browserEntrypoint).toEqualTypeOf<Entrypoint>(); +expectTypeOf(nodeEntrypoint).toEqualTypeOf<Entrypoint>(); +expectTypeOf(reactNativeEntrypoint).toEqualTypeOf<Entrypoint>(); + +expectTypeOf(browserEntrypoint).toEqualTypeOf(nodeEntrypoint); +expectTypeOf(browserEntrypoint).toEqualTypeOf(reactNativeEntrypoint); +expectTypeOf(nodeEntrypoint).toEqualTypeOf(reactNativeEntrypoint); diff --git a/lib/entrypoint.universal.test-d.ts b/lib/entrypoint.universal.test-d.ts new file mode 100644 index 000000000..184583a35 --- /dev/null +++ b/lib/entrypoint.universal.test-d.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expectTypeOf } from 'vitest'; + +import * as universal from './index.universal'; + +type WithoutReadonly<T> = { -readonly [P in keyof T]: T[P] }; + +const universalEntrypoint: WithoutReadonly<typeof universal> = universal; + +import { + Config, + Client, + StaticConfigManagerConfig, + OpaqueConfigManager, + EventDispatcher, + OpaqueEventProcessor, + OpaqueLevelPreset, + LoggerConfig, + OpaqueLogger, + ErrorHandler, + OpaqueErrorNotifier, +} from './export_types'; + +import { UniversalPollingConfigManagerConfig } from './project_config/config_manager_factory.universal'; +import { RequestHandler } from './utils/http_request_handler/http'; +import { UniversalBatchEventProcessorOptions } from './event_processor/event_processor_factory.universal'; +import { + DECISION_SOURCES, +} from './utils/enums'; + +import { NOTIFICATION_TYPES, DECISION_NOTIFICATION_TYPES } from './notification_center/type'; + +import { LogLevel } from './logging/logger'; + +import { OptimizelyDecideOption } from './shared_types'; +import { UniversalConfig } from './index.universal'; +import { OpaqueOdpManager } from './odp/odp_manager_factory'; + +import { UniversalOdpManagerOptions } from './odp/odp_manager_factory.universal'; + +export type UniversalEntrypoint = { + // client factory + createInstance: (config: UniversalConfig) => Client; + + // config manager related exports + createStaticProjectConfigManager: (config: StaticConfigManagerConfig) => OpaqueConfigManager; + createPollingProjectConfigManager: (config: UniversalPollingConfigManagerConfig) => OpaqueConfigManager; + + // event processor related exports + createEventDispatcher: (requestHandler: RequestHandler) => EventDispatcher; + createForwardingEventProcessor: (eventDispatcher: EventDispatcher) => OpaqueEventProcessor; + createBatchEventProcessor: (options: UniversalBatchEventProcessorOptions) => OpaqueEventProcessor; + + createOdpManager: (options: UniversalOdpManagerOptions) => OpaqueOdpManager; + + // TODO: vuid manager related exports + // createVuidManager: (options: VuidManagerOptions) => OpaqueVuidManager; + + // logger related exports + LogLevel: typeof LogLevel; + DEBUG: OpaqueLevelPreset, + INFO: OpaqueLevelPreset, + WARN: OpaqueLevelPreset, + ERROR: OpaqueLevelPreset, + createLogger: (config: LoggerConfig) => OpaqueLogger; + + // error related exports + createErrorNotifier: (errorHandler: ErrorHandler) => OpaqueErrorNotifier; + + // enums + DECISION_SOURCES: typeof DECISION_SOURCES; + NOTIFICATION_TYPES: typeof NOTIFICATION_TYPES; + DECISION_NOTIFICATION_TYPES: typeof DECISION_NOTIFICATION_TYPES; + + // decide options + OptimizelyDecideOption: typeof OptimizelyDecideOption; + + // client engine + clientEngine: string; +} + + +expectTypeOf(universalEntrypoint).toEqualTypeOf<UniversalEntrypoint>(); diff --git a/packages/optimizely-sdk/lib/plugins/error_handler/index.js b/lib/error/error_handler.ts similarity index 71% rename from packages/optimizely-sdk/lib/plugins/error_handler/index.js rename to lib/error/error_handler.ts index 105451f6a..4a772c71c 100644 --- a/packages/optimizely-sdk/lib/plugins/error_handler/index.js +++ b/lib/error/error_handler.ts @@ -1,5 +1,5 @@ /** - * Copyright 2016, Optimizely + * Copyright 2019, 2025, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /** - * Default error handler implementation + * @export + * @interface ErrorHandler */ -module.exports = { +export interface ErrorHandler { /** - * Handle given exception - * @param {Object} exception An exception object + * @param {Error} exception + * @memberof ErrorHandler */ - handleError: function(exception) { - // no-op - } -}; + handleError(exception: Error): void +} diff --git a/lib/error/error_notifier.spec.ts b/lib/error/error_notifier.spec.ts new file mode 100644 index 000000000..7c2b19d89 --- /dev/null +++ b/lib/error/error_notifier.spec.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; + +import { DefaultErrorNotifier } from './error_notifier'; +import { OptimizelyError } from './optimizly_error'; + +const mockMessageResolver = (prefix = '') => { + return { + resolve: vi.fn().mockImplementation((message) => `${prefix} ${message}`), + }; +} + +describe('DefaultErrorNotifier', () => { + it('should call the error handler with the error if the error is not an OptimizelyError', () => { + const errorHandler = { handleError: vi.fn() }; + const messageResolver = mockMessageResolver(); + const errorNotifier = new DefaultErrorNotifier(errorHandler, messageResolver); + + const error = new Error('error'); + errorNotifier.notify(error); + + expect(errorHandler.handleError).toHaveBeenCalledWith(error); + }); + + it('should resolve the message of an OptimizelyError before calling the error handler', () => { + const errorHandler = { handleError: vi.fn() }; + const messageResolver = mockMessageResolver('err'); + const errorNotifier = new DefaultErrorNotifier(errorHandler, messageResolver); + + const error = new OptimizelyError('test %s', 'one'); + errorNotifier.notify(error); + + expect(errorHandler.handleError).toHaveBeenCalledWith(error); + expect(error.message).toBe('err test one'); + }); +}); diff --git a/lib/error/error_notifier.ts b/lib/error/error_notifier.ts new file mode 100644 index 000000000..174c163e2 --- /dev/null +++ b/lib/error/error_notifier.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MessageResolver } from "../message/message_resolver"; +import { ErrorHandler } from "./error_handler"; +import { OptimizelyError } from "./optimizly_error"; + +export interface ErrorNotifier { + notify(error: Error): void; + child(name: string): ErrorNotifier; +} + +export class DefaultErrorNotifier implements ErrorNotifier { + private name: string; + private errorHandler: ErrorHandler; + private messageResolver: MessageResolver; + + constructor(errorHandler: ErrorHandler, messageResolver: MessageResolver, name?: string) { + this.errorHandler = errorHandler; + this.messageResolver = messageResolver; + this.name = name || ''; + } + + notify(error: Error): void { + if (error instanceof OptimizelyError) { + error.setMessage(this.messageResolver); + } + this.errorHandler.handleError(error); + } + + child(name: string): ErrorNotifier { + return new DefaultErrorNotifier(this.errorHandler, this.messageResolver, name); + } +} diff --git a/lib/error/error_notifier_factory.spec.ts b/lib/error/error_notifier_factory.spec.ts new file mode 100644 index 000000000..556d7f2af --- /dev/null +++ b/lib/error/error_notifier_factory.spec.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import { createErrorNotifier } from './error_notifier_factory'; + +describe('createErrorNotifier', () => { + it('should throw errors for invalid error handlers', () => { + expect(() => createErrorNotifier(null as any)).toThrow('Invalid error handler'); + expect(() => createErrorNotifier(undefined as any)).toThrow('Invalid error handler'); + + + expect(() => createErrorNotifier('abc' as any)).toThrow('Invalid error handler'); + expect(() => createErrorNotifier(123 as any)).toThrow('Invalid error handler'); + + expect(() => createErrorNotifier({} as any)).toThrow('Invalid error handler'); + + expect(() => createErrorNotifier({ handleError: 'abc' } as any)).toThrow('Invalid error handler'); + }); +}); diff --git a/lib/error/error_notifier_factory.ts b/lib/error/error_notifier_factory.ts new file mode 100644 index 000000000..994564f1a --- /dev/null +++ b/lib/error/error_notifier_factory.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { errorResolver } from "../message/message_resolver"; +import { Maybe } from "../utils/type"; +import { ErrorHandler } from "./error_handler"; +import { DefaultErrorNotifier } from "./error_notifier"; + +export const INVALID_ERROR_HANDLER = 'Invalid error handler'; + +const errorNotifierSymbol = Symbol(); + +export type OpaqueErrorNotifier = { + [errorNotifierSymbol]: unknown; +}; + +const validateErrorHandler = (errorHandler: ErrorHandler) => { + if (!errorHandler || typeof errorHandler !== 'object' || typeof errorHandler.handleError !== 'function') { + throw new Error(INVALID_ERROR_HANDLER); + } +} + +export const createErrorNotifier = (errorHandler: ErrorHandler): OpaqueErrorNotifier => { + validateErrorHandler(errorHandler); + return { + [errorNotifierSymbol]: new DefaultErrorNotifier(errorHandler, errorResolver), + } +} + +export const extractErrorNotifier = (errorNotifier: Maybe<OpaqueErrorNotifier>): Maybe<DefaultErrorNotifier> => { + if (!errorNotifier || typeof errorNotifier !== 'object') { + return undefined; + } + + return errorNotifier[errorNotifierSymbol] as Maybe<DefaultErrorNotifier>; +} diff --git a/lib/error/error_reporter.spec.ts b/lib/error/error_reporter.spec.ts new file mode 100644 index 000000000..abdd932d0 --- /dev/null +++ b/lib/error/error_reporter.spec.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; + +import { ErrorReporter } from './error_reporter'; + +import { OptimizelyError } from './optimizly_error'; + +const mockMessageResolver = (prefix = '') => { + return { + resolve: vi.fn().mockImplementation((message) => `${prefix} ${message}`), + }; +} + +describe('ErrorReporter', () => { + it('should call the logger and errorNotifier with the first argument if it is an Error object', () => { + const logger = { error: vi.fn() }; + const errorNotifier = { notify: vi.fn() }; + const errorReporter = new ErrorReporter(logger as any, errorNotifier as any); + + const error = new Error('error'); + errorReporter.report(error); + + expect(logger.error).toHaveBeenCalledWith(error); + expect(errorNotifier.notify).toHaveBeenCalledWith(error); + }); + + it('should create an OptimizelyError and call the logger and errorNotifier with it if the first argument is a string', () => { + const logger = { error: vi.fn() }; + const errorNotifier = { notify: vi.fn() }; + const errorReporter = new ErrorReporter(logger as any, errorNotifier as any); + + errorReporter.report('message', 1, 2); + + expect(logger.error).toHaveBeenCalled(); + const loggedError = logger.error.mock.calls[0][0]; + expect(loggedError).toBeInstanceOf(OptimizelyError); + expect(loggedError.baseMessage).toBe('message'); + expect(loggedError.params).toEqual([1, 2]); + + expect(errorNotifier.notify).toHaveBeenCalled(); + const notifiedError = errorNotifier.notify.mock.calls[0][0]; + expect(notifiedError).toBeInstanceOf(OptimizelyError); + expect(notifiedError.baseMessage).toBe('message'); + expect(notifiedError.params).toEqual([1, 2]); + }); +}); diff --git a/lib/error/error_reporter.ts b/lib/error/error_reporter.ts new file mode 100644 index 000000000..130527928 --- /dev/null +++ b/lib/error/error_reporter.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from "../logging/logger"; +import { ErrorNotifier } from "./error_notifier"; +import { OptimizelyError } from "./optimizly_error"; + +export class ErrorReporter { + private logger?: LoggerFacade; + private errorNotifier?: ErrorNotifier; + + constructor(logger?: LoggerFacade, errorNotifier?: ErrorNotifier) { + this.logger = logger; + this.errorNotifier = errorNotifier; + } + + report(error: Error): void; + report(baseMessage: string, ...params: any[]): void; + + report(error: Error | string, ...params: any[]): void { + if (typeof error === 'string') { + error = new OptimizelyError(error, ...params); + this.report(error); + return; + } + + if (this.errorNotifier) { + this.errorNotifier.notify(error); + } + + if (this.logger) { + this.logger.error(error); + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + } + + setErrorNotifier(errorNotifier: ErrorNotifier): void { + this.errorNotifier = errorNotifier; + } +} diff --git a/lib/error/optimizly_error.ts b/lib/error/optimizly_error.ts new file mode 100644 index 000000000..76a07511a --- /dev/null +++ b/lib/error/optimizly_error.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MessageResolver } from "../message/message_resolver"; +import { sprintf } from "../utils/fns"; + +export class OptimizelyError extends Error { + baseMessage: string; + params: any[]; + private resolved = false; + constructor(baseMessage: string, ...params: any[]) { + super(); + this.name = 'OptimizelyError'; + this.baseMessage = baseMessage; + this.params = params; + + // this is needed cause instanceof doesn't work for + // custom Errors when TS is compiled to es5 + Object.setPrototypeOf(this, OptimizelyError.prototype); + } + + setMessage(resolver: MessageResolver): void { + if (!this.resolved) { + this.message = sprintf(resolver.resolve(this.baseMessage), ...this.params); + this.resolved = true; + } + } +} diff --git a/lib/event_processor/batch_event_processor.react_native.spec.ts b/lib/event_processor/batch_event_processor.react_native.spec.ts new file mode 100644 index 000000000..5e17ca966 --- /dev/null +++ b/lib/event_processor/batch_event_processor.react_native.spec.ts @@ -0,0 +1,169 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +const mockNetInfo = vi.hoisted(() => { + const netInfo = { + listeners: [] as any[], + unsubs: [] as any[], + addEventListener(fn: any) { + this.listeners.push(fn); + const unsub = vi.fn(); + this.unsubs.push(unsub); + return unsub; + }, + pushState(state: boolean) { + for (const listener of this.listeners) { + listener({ isInternetReachable: state }); + } + }, + clear() { + this.listeners = []; + this.unsubs = []; + } + }; + return netInfo; +}); + +vi.mock('@react-native-community/netinfo', () => { + return { + addEventListener: mockNetInfo.addEventListener.bind(mockNetInfo), + }; +}); + +import { ReactNativeNetInfoEventProcessor } from './batch_event_processor.react_native'; +import { getMockRepeater } from '../tests/mock/mock_repeater'; +import { getMockAsyncCache } from '../tests/mock/mock_cache'; + +import { EventWithId } from './batch_event_processor'; +import { buildLogEvent } from './event_builder/log_event'; +import { createImpressionEvent } from '../tests/mock/create_event'; +import { ProcessableEvent } from './event_processor'; + +const getMockDispatcher = () => { + return { + dispatchEvent: vi.fn(), + }; +}; + +const exhaustMicrotasks = async (loop = 100) => { + for(let i = 0; i < loop; i++) { + await Promise.resolve(); + } +} + + +describe('ReactNativeNetInfoEventProcessor', () => { + beforeEach(() => { + mockNetInfo.clear(); + }); + + it('should not retry failed events when reachable state does not change', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const cache = getMockAsyncCache<EventWithId>(); + const events: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + events.push(event); + await cache.set(id, { id, event }); + } + + const processor = new ReactNativeNetInfoEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 1000, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + mockNetInfo.pushState(true); + expect(eventDispatcher.dispatchEvent).not.toHaveBeenCalled(); + + mockNetInfo.pushState(true); + expect(eventDispatcher.dispatchEvent).not.toHaveBeenCalled(); + }); + + it('should retry failed events when network becomes reachable', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const cache = getMockAsyncCache<EventWithId>(); + const events: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + events.push(event); + await cache.set(id, { id, event }); + } + + const processor = new ReactNativeNetInfoEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 1000, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + mockNetInfo.pushState(false); + expect(eventDispatcher.dispatchEvent).not.toHaveBeenCalled(); + + mockNetInfo.pushState(true); + + await exhaustMicrotasks(); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); + }); + + it('should unsubscribe from netinfo listener when stopped', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const cache = getMockAsyncCache<EventWithId>(); + + const processor = new ReactNativeNetInfoEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 1000, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + mockNetInfo.pushState(false); + + processor.stop(); + await processor.onTerminated(); + + expect(mockNetInfo.unsubs[0]).toHaveBeenCalled(); + }); +}); diff --git a/lib/event_processor/batch_event_processor.react_native.ts b/lib/event_processor/batch_event_processor.react_native.ts new file mode 100644 index 000000000..28741380a --- /dev/null +++ b/lib/event_processor/batch_event_processor.react_native.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NetInfoState, addEventListener } from '@react-native-community/netinfo'; + +import { BatchEventProcessor, BatchEventProcessorConfig } from './batch_event_processor'; +import { Fn } from '../utils/type'; + +export class ReactNativeNetInfoEventProcessor extends BatchEventProcessor { + private isInternetReachable = true; + private unsubscribeNetInfo?: Fn; + + constructor(config: BatchEventProcessorConfig) { + super(config); + } + + private async connectionListener(state: NetInfoState) { + if (this.isInternetReachable && !state.isInternetReachable) { + this.isInternetReachable = false; + return; + } + + if (!this.isInternetReachable && state.isInternetReachable) { + this.isInternetReachable = true; + this.retryFailedEvents(); + } + } + + start(): void { + super.start(); + this.unsubscribeNetInfo = addEventListener(this.connectionListener.bind(this)); + } + + stop(): void { + this.unsubscribeNetInfo?.(); + super.stop(); + } +} diff --git a/lib/event_processor/batch_event_processor.spec.ts b/lib/event_processor/batch_event_processor.spec.ts new file mode 100644 index 000000000..6d7674fd5 --- /dev/null +++ b/lib/event_processor/batch_event_processor.spec.ts @@ -0,0 +1,1483 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, describe, it, vi, beforeEach, afterEach, MockInstance } from 'vitest'; + +import { EventWithId, BatchEventProcessor, LOGGER_NAME } from './batch_event_processor'; +import { getMockAsyncCache, getMockSyncCache } from '../tests/mock/mock_cache'; +import { createImpressionEvent } from '../tests/mock/create_event'; +import { ProcessableEvent } from './event_processor'; +import { buildLogEvent } from './event_builder/log_event'; +import { ResolvablePromise, resolvablePromise } from '../utils/promise/resolvablePromise'; +import { advanceTimersByTime } from '../tests/testUtils'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { getMockRepeater } from '../tests/mock/mock_repeater'; +import * as retry from '../utils/executor/backoff_retry_runner'; +import { ServiceState, StartupLog } from '../service'; +import { LogLevel } from '../logging/logger'; +import { IdGenerator } from '../utils/id_generator'; + +const getMockDispatcher = () => { + return { + dispatchEvent: vi.fn(), + }; +}; + +const exhaustMicrotasks = async (loop = 100) => { + for(let i = 0; i < loop; i++) { + await Promise.resolve(); + } +} + +describe('BatchEventProcessor', async () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should set name on the logger passed into the constructor', () => { + const logger = getMockLogger(); + + const processor = new BatchEventProcessor({ + eventDispatcher: getMockDispatcher(), + dispatchRepeater: getMockRepeater(), + batchSize: 1000, + logger, + }); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should set name on the logger set by setLogger', () => { + const logger = getMockLogger(); + + const processor = new BatchEventProcessor({ + eventDispatcher: getMockDispatcher(), + dispatchRepeater: getMockRepeater(), + batchSize: 1000, + }); + + processor.setLogger(logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + describe('start', () => { + it('should log startupLogs on start', () => { + const startupLogs: StartupLog[] = [ + { + level: LogLevel.Warn, + message: 'warn message', + params: [1, 2] + }, + { + level: LogLevel.Error, + message: 'error message', + params: [3, 4] + }, + ]; + + const logger = getMockLogger(); + + const processor = new BatchEventProcessor({ + eventDispatcher: getMockDispatcher(), + dispatchRepeater: getMockRepeater(), + batchSize: 1000, + startupLogs, + }); + + processor.setLogger(logger); + processor.start(); + + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith('warn message', 1, 2); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith('error message', 3, 4); + }); + + it('should resolve onRunning() when start() is called', async () => { + const eventDispatcher = getMockDispatcher(); + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 1000, + }); + + processor.start(); + await expect(processor.onRunning()).resolves.not.toThrow(); + }); + + it('should start failedEventRepeater', () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 1000, + }); + + processor.start(); + expect(failedEventRepeater.start).toHaveBeenCalledOnce(); + }); + + it('should dispatch failed events in correct batch sizes and order', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const cache = getMockSyncCache<EventWithId>(); + const events: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + events.push(event); + cache.set(id, { id, event }); + } + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 2, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + await exhaustMicrotasks(); + + expect(mockDispatch).toHaveBeenCalledTimes(3); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([events[0], events[1]])); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent([events[2], events[3]])); + expect(mockDispatch.mock.calls[2][0]).toEqual(buildLogEvent([events[4]])); + }); + }); + + describe('process', () => { + it('should return a promise that rejects if processor is not running', async () => { + const eventDispatcher = getMockDispatcher(); + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 100, + }); + + await expect(processor.process(createImpressionEvent('id-1'))).rejects.toThrow(); + }); + + it('should enqueue event without dispatching immediately', async () => { + const eventDispatcher = getMockDispatcher(); + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + for(let i = 0; i < 99; i++) { + const event = createImpressionEvent(`id-${i}`); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + }); + + it('should start the dispatchRepeater if it is not running', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const event = createImpressionEvent('id-1'); + await processor.process(event); + + expect(dispatchRepeater.start).toHaveBeenCalledOnce(); + }); + + it('should dispatch events if queue is full and clear queue', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + let events: ProcessableEvent[] = []; + for(let i = 0; i < 99; i++){ + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + let event = createImpressionEvent('id-99'); + events.push(event); + await processor.process(event); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); + + events = []; + + for(let i = 100; i < 199; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + + event = createImpressionEvent('id-199'); + events.push(event); + await processor.process(event); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); + expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(buildLogEvent(events)); + }); + + it('should flush queue is context of the new event is different and enqueue the new event', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 80; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + const newEvent = createImpressionEvent('id-a'); + newEvent.context.accountId = 'account-' + Math.random(); + await processor.process(newEvent); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); + + await dispatchRepeater.execute(0); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); + expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(buildLogEvent([newEvent])); + }); + + it('should flush queue immediately regardless of batchSize, if event processor is disposable', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.makeDisposable(); + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + const event = createImpressionEvent('id-1'); + events.push(event); + await processor.process(event); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); + expect(dispatchRepeater.reset).toHaveBeenCalledTimes(1); + expect(dispatchRepeater.start).not.toHaveBeenCalled(); + expect(failedEventRepeater.start).not.toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(processor.retryConfig?.maxRetries).toEqual(5); + }); + + it('should store the event in the eventStore with increasing ids', async () => { + const eventDispatcher = getMockDispatcher(); + const eventStore = getMockSyncCache<EventWithId>(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 100, + eventStore, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(10); + + const eventsInStore = Array.from(eventStore.getAll().values()) + .sort((a, b) => a < b ? -1 : 1).map(e => e.event); + + expect(events).toEqual(eventsInStore); + }); + + it('should not store the event in the eventStore but still dispatch if the \ + number of pending events is greater than the limit', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue(resolvablePromise().promise); + + const eventStore = getMockSyncCache<EventWithId>(); + + const idGenerator = new IdGenerator(); + + for (let i = 0; i < 505; i++) { + const event = createImpressionEvent(`id-${i}`); + const cacheId = idGenerator.getId(); + await eventStore.set(cacheId, { id: cacheId, event }); + } + + expect(eventStore.size()).toEqual(505); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 1, + eventStore, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 2; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(505); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(507); + expect(eventDispatcher.dispatchEvent.mock.calls[505][0]).toEqual(buildLogEvent([events[0]])); + expect(eventDispatcher.dispatchEvent.mock.calls[506][0]).toEqual(buildLogEvent([events[1]])); + }); + + it('should store events in the eventStore when the number of events in the store\ + becomes lower than the limit', async () => { + const eventDispatcher = getMockDispatcher(); + + const dispatchResponses: ResolvablePromise<any>[] = []; + + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockImplementation((arg) => { + const dispatchResponse = resolvablePromise(); + dispatchResponses.push(dispatchResponse); + return dispatchResponse.promise; + }); + + const eventStore = getMockSyncCache<EventWithId>(); + + const idGenerator = new IdGenerator(); + + for (let i = 0; i < 502; i++) { + const event = createImpressionEvent(`id-${i}`); + const cacheId = String(i); + await eventStore.set(cacheId, { id: cacheId, event }); + } + + expect(eventStore.size()).toEqual(502); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater: getMockRepeater(), + batchSize: 1, + eventStore, + }); + + processor.start(); + await processor.onRunning(); + + let events: ProcessableEvent[] = []; + for(let i = 0; i < 2; i++) { + const event = createImpressionEvent(`id-${i + 502}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(502); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(504); + + expect(eventDispatcher.dispatchEvent.mock.calls[502][0]).toEqual(buildLogEvent([events[0]])); + expect(eventDispatcher.dispatchEvent.mock.calls[503][0]).toEqual(buildLogEvent([events[1]])); + + // resolve the dispatch for events not saved in the store + dispatchResponses[502].resolve({ statusCode: 200 }); + dispatchResponses[503].resolve({ statusCode: 200 }); + + await exhaustMicrotasks(); + expect(eventStore.size()).toEqual(502); + + // resolve the dispatch for 3 events in store, making the store size 499 which is lower than the limit + dispatchResponses[0].resolve({ statusCode: 200 }); + dispatchResponses[1].resolve({ statusCode: 200 }); + dispatchResponses[2].resolve({ statusCode: 200 }); + + await exhaustMicrotasks(); + expect(eventStore.size()).toEqual(499); + + // process 2 more events + events = []; + for(let i = 0; i < 2; i++) { + const event = createImpressionEvent(`id-${i + 504}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(500); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(506); + expect(eventDispatcher.dispatchEvent.mock.calls[504][0]).toEqual(buildLogEvent([events[0]])); + expect(eventDispatcher.dispatchEvent.mock.calls[505][0]).toEqual(buildLogEvent([events[1]])); + }); + + it('should still dispatch events even if the store save fails', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const eventStore = getMockAsyncCache<EventWithId>(); + // Simulate failure in saving to store + eventStore.set = vi.fn().mockRejectedValue(new Error('Failed to save')); + + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + eventStore, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event) + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + await dispatchRepeater.execute(0); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); + }); + }); + + it('should dispatch events when dispatchRepeater is triggered', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + let events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + await dispatchRepeater.execute(0); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); + + events = []; + for(let i = 1; i < 15; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + await dispatchRepeater.execute(0); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); + expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(buildLogEvent(events)); + }); + + it('should not retry failed dispatch if retryConfig is not provided', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockRejectedValue(new Error()); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + await dispatchRepeater.execute(0); + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + }); + + it('should retry specified number of times using the provided backoffController', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockRejectedValue(new Error()); + const dispatchRepeater = getMockRepeater(); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1000), + reset: vi.fn(), + }; + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + retryConfig: { + backoffProvider: () => backoffController, + maxRetries: 3, + }, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + await dispatchRepeater.execute(0); + + for(let i = 0; i < 10; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(1000); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(4); + expect(backoffController.backoff).toHaveBeenCalledTimes(3); + + const request = buildLogEvent(events); + for(let i = 0; i < 4; i++) { + expect(eventDispatcher.dispatchEvent.mock.calls[i][0]).toEqual(request); + } + }); + + it('should remove the events from the eventStore after dispatch is successfull', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + const dispatchResponse = resolvablePromise(); + + mockDispatch.mockResolvedValue(dispatchResponse.promise); + + const eventStore = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + eventStore, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(10); + await dispatchRepeater.execute(0); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + // the dispatch is not resolved yet, so all the events should still be in the store + expect(eventStore.size()).toEqual(10); + + dispatchResponse.resolve({ statusCode: 200 }); + + await exhaustMicrotasks(); + + expect(eventStore.size()).toEqual(0); + }); + + it('should remove the events from the eventStore after dispatch is successfull', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + const dispatchResponse = resolvablePromise(); + + mockDispatch.mockResolvedValue(dispatchResponse.promise); + + const eventStore = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + eventStore, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(10); + await dispatchRepeater.execute(0); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + // the dispatch is not resolved yet, so all the events should still be in the store + expect(eventStore.size()).toEqual(10); + + dispatchResponse.resolve({ statusCode: 200 }); + + await exhaustMicrotasks(); + + expect(eventStore.size()).toEqual(0); + }); + + it('should remove the events from the eventStore after dispatch is successfull after retries', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + + mockDispatch.mockResolvedValueOnce({ statusCode: 500 }) + .mockResolvedValueOnce({ statusCode: 500 }) + .mockResolvedValueOnce({ statusCode: 200 }); + + const eventStore = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1000), + reset: vi.fn(), + }; + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + eventStore, + retryConfig: { + backoffProvider: () => backoffController, + maxRetries: 3, + }, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event) + } + + expect(eventStore.size()).toEqual(10); + await dispatchRepeater.execute(0); + + for(let i = 0; i < 10; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(1000); + } + + expect(mockDispatch).toHaveBeenCalledTimes(3); + expect(eventStore.size()).toEqual(0); + }); + + it('should log error and keep events in store if dispatch return 5xx response', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({ statusCode: 500 }); + const dispatchRepeater = getMockRepeater(); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1000), + reset: vi.fn(), + }; + + const eventStore = getMockSyncCache<EventWithId>(); + const logger = getMockLogger(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + eventStore, + retryConfig: { + backoffProvider: () => backoffController, + maxRetries: 3, + }, + batchSize: 100, + logger, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + expect(eventStore.size()).toEqual(10); + + await dispatchRepeater.execute(0); + + for(let i = 0; i < 10; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(1000); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(4); + expect(backoffController.backoff).toHaveBeenCalledTimes(3); + expect(eventStore.size()).toEqual(10); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should log error and keep events in store if dispatch promise fails', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockRejectedValue(new Error()); + const dispatchRepeater = getMockRepeater(); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1000), + reset: vi.fn(), + }; + + const eventStore = getMockSyncCache<EventWithId>(); + const logger = getMockLogger(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + eventStore, + retryConfig: { + backoffProvider: () => backoffController, + maxRetries: 3, + }, + batchSize: 100, + logger, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + expect(eventStore.size()).toEqual(10); + + await dispatchRepeater.execute(0); + + for(let i = 0; i < 10; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(1000); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(4); + expect(backoffController.backoff).toHaveBeenCalledTimes(3); + expect(eventStore.size()).toEqual(10); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + describe('retryFailedEvents', () => { + it('should dispatch only failed events from the store and not dispatch queued events', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const cache = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + // these events should be in queue and should not be reomoved from store or dispatched with failed events + const eventA = createImpressionEvent('id-A'); + const eventB = createImpressionEvent('id-B'); + await processor.process(eventA); + await processor.process(eventB); + + const failedEvents: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + failedEvents.push(event); + cache.set(id, { id, event }); + } + + await processor.retryFailedEvents(); + await exhaustMicrotasks(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent(failedEvents)); + + const eventsInStore = [...cache.getAll().values()].sort((a, b) => a.id < b.id ? -1 : 1).map(e => e.event); + expect(eventsInStore).toEqual(expect.arrayContaining([ + expect.objectContaining(eventA), + expect.objectContaining(eventB), + ])); + }); + + it('should dispatch only failed events from the store and not dispatch events that are being dispatched', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + const mockResult1 = resolvablePromise(); + const mockResult2 = resolvablePromise(); + mockDispatch.mockResolvedValueOnce(mockResult1.promise).mockRejectedValueOnce(mockResult2.promise); + + const cache = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + // these events should be in dispatch and should not be reomoved from store or dispatched with failed events + const eventA = createImpressionEvent('id-A'); + const eventB = createImpressionEvent('id-B'); + await processor.process(eventA); + await processor.process(eventB); + + dispatchRepeater.execute(0); + await exhaustMicrotasks(); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([eventA, eventB])); + + const failedEvents: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + failedEvents.push(event); + cache.set(id, { id, event }); + } + + await processor.retryFailedEvents(); + await exhaustMicrotasks(); + + expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent(failedEvents)); + + mockResult2.resolve({}); + await exhaustMicrotasks(); + + const eventsInStore = [...cache.getAll().values()].sort((a, b) => a.id < b.id ? -1 : 1).map(e => e.event); + expect(eventsInStore).toEqual(expect.arrayContaining([ + expect.objectContaining(eventA), + expect.objectContaining(eventB), + ])); + }); + + it('should dispatch events in correct batch size and separate events with differnt contexts in separate batch', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const cache = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 3, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + const failedEvents: ProcessableEvent[] = []; + + for(let i = 0; i < 8; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + + if (i == 2 || i == 3) { + event.context.accountId = 'new-account'; + } + + failedEvents.push(event); + cache.set(id, { id, event }); + } + + await processor.retryFailedEvents(); + await exhaustMicrotasks(); + + // events 0 1 4 5 6 7 have one context, and 2 3 have different context + // batches should be [0, 1], [2, 3], [4, 5, 6], [7] + expect(mockDispatch).toHaveBeenCalledTimes(4); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([failedEvents[0], failedEvents[1]])); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent([failedEvents[2], failedEvents[3]])); + expect(mockDispatch.mock.calls[2][0]).toEqual(buildLogEvent([failedEvents[4], failedEvents[5], failedEvents[6]])); + expect(mockDispatch.mock.calls[3][0]).toEqual(buildLogEvent([failedEvents[7]])); + }); + }); + + describe('when failedEventRepeater is fired', () => { + it('should dispatch only failed events from the store and not dispatch queued events', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const cache = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + // these events should be in queue and should not be reomoved from store or dispatched with failed events + const eventA = createImpressionEvent('id-A'); + const eventB = createImpressionEvent('id-B'); + await processor.process(eventA); + await processor.process(eventB); + + const failedEvents: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + failedEvents.push(event); + cache.set(id, { id, event }); + } + + failedEventRepeater.execute(0); + await exhaustMicrotasks(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent(failedEvents)); + + const eventsInStore = [...cache.getAll().values()].sort((a, b) => a.id < b.id ? -1 : 1).map(e => e.event); + expect(eventsInStore).toEqual(expect.arrayContaining([ + expect.objectContaining(eventA), + expect.objectContaining(eventB), + ])); + }); + + it('should dispatch only failed events from the store and not dispatch events that are being dispatched', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + const mockResult1 = resolvablePromise(); + const mockResult2 = resolvablePromise(); + mockDispatch.mockResolvedValueOnce(mockResult1.promise).mockRejectedValueOnce(mockResult2.promise); + + const cache = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + // these events should be in dispatch and should not be reomoved from store or dispatched with failed events + const eventA = createImpressionEvent('id-A'); + const eventB = createImpressionEvent('id-B'); + await processor.process(eventA); + await processor.process(eventB); + + dispatchRepeater.execute(0); + await exhaustMicrotasks(); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([eventA, eventB])); + + const failedEvents: ProcessableEvent[] = []; + + for(let i = 0; i < 5; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + failedEvents.push(event); + cache.set(id, { id, event }); + } + + failedEventRepeater.execute(0); + await exhaustMicrotasks(); + + expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent(failedEvents)); + + mockResult2.resolve({}); + await exhaustMicrotasks(); + + const eventsInStore = [...cache.getAll().values()].sort((a, b) => a.id < b.id ? -1 : 1).map(e => e.event); + expect(eventsInStore).toEqual(expect.arrayContaining([ + expect.objectContaining(eventA), + expect.objectContaining(eventB), + ])); + }); + + it('should dispatch events in correct batch size and separate events with differnt contexts in separate batch', async () => { + const eventDispatcher = getMockDispatcher(); + const mockDispatch: MockInstance<typeof eventDispatcher.dispatchEvent> = eventDispatcher.dispatchEvent; + mockDispatch.mockResolvedValue({}); + + const cache = getMockSyncCache<EventWithId>(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 3, + eventStore: cache, + }); + + processor.start(); + await processor.onRunning(); + + const failedEvents: ProcessableEvent[] = []; + + for(let i = 0; i < 8; i++) { + const id = `id-${i}`; + const event = createImpressionEvent(id); + + if (i == 2 || i == 3) { + event.context.accountId = 'new-account'; + } + + failedEvents.push(event); + cache.set(id, { id, event }); + } + + failedEventRepeater.execute(0); + await exhaustMicrotasks(); + + // events 0 1 4 5 6 7 have one context, and 2 3 have different context + // batches should be [0, 1], [2, 3], [4, 5, 6], [7] + expect(mockDispatch).toHaveBeenCalledTimes(4); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([failedEvents[0], failedEvents[1]])); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent([failedEvents[2], failedEvents[3]])); + expect(mockDispatch.mock.calls[2][0]).toEqual(buildLogEvent([failedEvents[4], failedEvents[5], failedEvents[6]])); + expect(mockDispatch.mock.calls[3][0]).toEqual(buildLogEvent([failedEvents[7]])); + }); + }); + + it('should emit dispatch event when dispatching events', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + const event = createImpressionEvent('id-1'); + const event2 = createImpressionEvent('id-2'); + + const dispatchListener = vi.fn(); + processor.onDispatch(dispatchListener); + + processor.start(); + await processor.onRunning(); + + await processor.process(event); + await processor.process(event2); + + await dispatchRepeater.execute(0); + + expect(dispatchListener).toHaveBeenCalledTimes(1); + expect(dispatchListener.mock.calls[0][0]).toEqual(buildLogEvent([event, event2])); + }); + + it('should remove event handler when function returned from onDispatch is called', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + const dispatchListener = vi.fn(); + + const unsub = processor.onDispatch(dispatchListener); + + processor.start(); + await processor.onRunning(); + + const event = createImpressionEvent('id-1'); + const event2 = createImpressionEvent('id-2'); + + await processor.process(event); + await processor.process(event2); + + await dispatchRepeater.execute(0); + + expect(dispatchListener).toHaveBeenCalledTimes(1); + expect(dispatchListener.mock.calls[0][0]).toEqual(buildLogEvent([event, event2])); + + unsub(); + + const event3 = createImpressionEvent('id-3'); + const event4 = createImpressionEvent('id-4'); + + await dispatchRepeater.execute(0); + expect(dispatchListener).toHaveBeenCalledTimes(1); + }); + + describe('stop', () => { + it('should reject onRunning if stop is called before the processor is started', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + processor.stop(); + + await expect(processor.onRunning()).rejects.toThrow(); + }); + + it('should stop dispatchRepeater and failedEventRepeater', async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + processor.stop(); + expect(dispatchRepeater.stop).toHaveBeenCalledOnce(); + expect(failedEventRepeater.stop).toHaveBeenCalledOnce(); + }); + + it('should dispatch the events in queue using the closing dispatcher if available', async () => { + const eventDispatcher = getMockDispatcher(); + const closingEventDispatcher = getMockDispatcher(); + closingEventDispatcher.dispatchEvent.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + closingEventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + processor.stop(); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); + }); + + it('should cancel retry of active dispatches', async () => { + const runWithRetrySpy = vi.spyOn(retry, 'runWithRetry'); + const cancel1 = vi.fn(); + const cancel2 = vi.fn(); + runWithRetrySpy.mockReturnValueOnce({ + cancelRetry: cancel1, + result: resolvablePromise().promise, + }).mockReturnValueOnce({ + cancelRetry: cancel2, + result: resolvablePromise().promise, + }); + + const eventDispatcher = getMockDispatcher(); + const dispatchRepeater = getMockRepeater(); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1000), + reset: vi.fn(), + }; + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + retryConfig: { + backoffProvider: () => backoffController, + maxRetries: 3, + } + }); + + processor.start(); + await processor.onRunning(); + + await processor.process(createImpressionEvent('id-1')); + await dispatchRepeater.execute(0); + + expect(runWithRetrySpy).toHaveBeenCalledTimes(1); + + await processor.process(createImpressionEvent('id-2')); + await dispatchRepeater.execute(0); + + expect(runWithRetrySpy).toHaveBeenCalledTimes(2); + + processor.stop(); + + expect(cancel1).toHaveBeenCalledOnce(); + expect(cancel2).toHaveBeenCalledOnce(); + + runWithRetrySpy.mockReset(); + }); + + it('should resolve onTerminated when all active dispatch requests settles' , async () => { + const eventDispatcher = getMockDispatcher(); + const dispatchRes1 = resolvablePromise<void>(); + const dispatchRes2 = resolvablePromise<void>(); + eventDispatcher.dispatchEvent.mockReturnValueOnce(dispatchRes1.promise) + .mockReturnValueOnce(dispatchRes2.promise); + + const dispatchRepeater = getMockRepeater(); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1000), + reset: vi.fn(), + }; + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + batchSize: 100, + }); + + processor.start() + await processor.onRunning(); + + await processor.process(createImpressionEvent('id-1')); + await dispatchRepeater.execute(0); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + + await processor.process(createImpressionEvent('id-2')); + await dispatchRepeater.execute(0); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); + + const onStop = vi.fn(); + processor.onTerminated().then(onStop); + + processor.stop(); + + await exhaustMicrotasks(); + expect(onStop).not.toHaveBeenCalled(); + expect(processor.getState()).toEqual(ServiceState.Stopping); + + dispatchRes1.resolve(); + dispatchRes2.reject(new Error()); + + await expect(processor.onTerminated()).resolves.not.toThrow(); + }); + }); + + describe('flushImmediately', () => { + it('should dispatch the events in queue using the closing dispatcher if available', async () => { + const eventDispatcher = getMockDispatcher(); + const closingEventDispatcher = getMockDispatcher(); + closingEventDispatcher.dispatchEvent.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + closingEventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + processor.flushImmediately(); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); + + expect(processor.isRunning()).toBe(true); + }); + + + it('should dispatch the events in queue using eventDispatcher if closingEventDispatcher is not available', async () => { + const eventDispatcher = getMockDispatcher(); + eventDispatcher.dispatchEvent.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + processor.flushImmediately(); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); + + expect(processor.isRunning()).toBe(true); + }); + }); +}); diff --git a/lib/event_processor/batch_event_processor.ts b/lib/event_processor/batch_event_processor.ts new file mode 100644 index 000000000..86f7ff148 --- /dev/null +++ b/lib/event_processor/batch_event_processor.ts @@ -0,0 +1,365 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventProcessor, ProcessableEvent } from "./event_processor"; +import { getBatchedAsync, getBatchedSync, Store } from "../utils/cache/store"; +import { EventDispatcher, EventDispatcherResponse, LogEvent } from "./event_dispatcher/event_dispatcher"; +import { buildLogEvent } from "./event_builder/log_event"; +import { BackoffController, ExponentialBackoff, Repeater } from "../utils/repeater/repeater"; +import { LoggerFacade } from '../logging/logger'; +import { BaseService, ServiceState, StartupLog } from "../service"; +import { Consumer, Fn, Maybe, Producer } from "../utils/type"; +import { RunResult, runWithRetry } from "../utils/executor/backoff_retry_runner"; +import { isSuccessStatusCode } from "../utils/http_request_handler/http_util"; +import { EventEmitter } from "../utils/event_emitter/event_emitter"; +import { IdGenerator } from "../utils/id_generator"; +import { areEventContextsEqual } from "./event_builder/user_event"; +import { FAILED_TO_DISPATCH_EVENTS, SERVICE_NOT_RUNNING } from "error_message"; +import { OptimizelyError } from "../error/optimizly_error"; +import { sprintf } from "../utils/fns"; +import { SERVICE_STOPPED_BEFORE_RUNNING } from "../service"; +import { EVENT_STORE_FULL } from "../message/log_message"; + +export const DEFAULT_MIN_BACKOFF = 1000; +export const DEFAULT_MAX_BACKOFF = 32000; +export const MAX_EVENTS_IN_STORE = 500; + +export type EventWithId = { + id: string; + event: ProcessableEvent; + notStored?: boolean; +}; + +export type RetryConfig = { + maxRetries: number; + backoffProvider: Producer<BackoffController>; +} + +export type BatchEventProcessorConfig = { + dispatchRepeater: Repeater, + failedEventRepeater?: Repeater, + batchSize: number, + eventStore?: Store<EventWithId>, + eventDispatcher: EventDispatcher, + closingEventDispatcher?: EventDispatcher, + logger?: LoggerFacade, + retryConfig?: RetryConfig; + startupLogs?: StartupLog[]; +}; + +type EventBatch = { + request: LogEvent, + events: EventWithId[], +} + +export const LOGGER_NAME = 'BatchEventProcessor'; + +export class BatchEventProcessor extends BaseService implements EventProcessor { + private eventDispatcher: EventDispatcher; + private closingEventDispatcher?: EventDispatcher; + private eventQueue: EventWithId[] = []; + private batchSize: number; + private eventStore?: Store<EventWithId>; + private eventCountInStore: Maybe<number> = undefined; + private eventCountWaitPromise: Promise<unknown> = Promise.resolve(); + private maxEventsInStore: number = MAX_EVENTS_IN_STORE; + private dispatchRepeater: Repeater; + private failedEventRepeater?: Repeater; + private idGenerator: IdGenerator = new IdGenerator(); + private runningTask: Map<string, RunResult<EventDispatcherResponse>> = new Map(); + private dispatchingEvents: Map<string, EventWithId> = new Map(); + private eventEmitter: EventEmitter<{ dispatch: LogEvent }> = new EventEmitter(); + private retryConfig?: RetryConfig; + + constructor(config: BatchEventProcessorConfig) { + super(config.startupLogs); + this.eventDispatcher = config.eventDispatcher; + this.closingEventDispatcher = config.closingEventDispatcher; + this.batchSize = config.batchSize; + this.eventStore = config.eventStore; + + this.retryConfig = config.retryConfig; + + this.dispatchRepeater = config.dispatchRepeater; + this.dispatchRepeater.setTask(() => this.flush()); + + this.maxEventsInStore = Math.max(2 * config.batchSize, MAX_EVENTS_IN_STORE); + this.failedEventRepeater = config.failedEventRepeater; + this.failedEventRepeater?.setTask(() => this.retryFailedEvents()); + if (config.logger) { + this.setLogger(config.logger); + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + } + + onDispatch(handler: Consumer<LogEvent>): Fn { + return this.eventEmitter.on('dispatch', handler); + } + + public async retryFailedEvents(): Promise<void> { + if (!this.eventStore) { + return; + } + + const keys = (await this.eventStore.getKeys()).filter( + (k) => !this.dispatchingEvents.has(k) && !this.eventQueue.find((e) => e.id === k) + ); + + const events = await (this.eventStore.operation === 'sync' ? + getBatchedSync(this.eventStore, keys) : getBatchedAsync(this.eventStore, keys)); + + const failedEvents: EventWithId[] = []; + events.forEach((e) => { + if(e) { + failedEvents.push(e); + } + }); + + if (failedEvents.length == 0) { + return; + } + + failedEvents.sort((a, b) => a.id < b.id ? -1 : 1); + + const batches: EventBatch[] = []; + let currentBatch: EventWithId[] = []; + + failedEvents.forEach((event) => { + if (currentBatch.length === this.batchSize || + (currentBatch.length > 0 && !areEventContextsEqual(currentBatch[0].event, event.event))) { + batches.push({ + request: buildLogEvent(currentBatch.map((e) => e.event)), + events: currentBatch, + }); + currentBatch = []; + } + currentBatch.push(event); + }); + + if (currentBatch.length > 0) { + batches.push({ + request: buildLogEvent(currentBatch.map((e) => e.event)), + events: currentBatch, + }); + } + + batches.forEach((batch) => { + this.dispatchBatch(batch, false); + }); + } + + private createNewBatch(): EventBatch | undefined { + if (this.eventQueue.length == 0) { + return + } + + const events: ProcessableEvent[] = []; + const eventWithIds: EventWithId[] = []; + + this.eventQueue.forEach((event) => { + events.push(event.event); + eventWithIds.push(event); + }); + + this.eventQueue = []; + return { request: buildLogEvent(events), events: eventWithIds }; + } + + private async executeDispatch(request: LogEvent, closing = false): Promise<EventDispatcherResponse> { + const dispatcher = closing && this.closingEventDispatcher ? this.closingEventDispatcher : this.eventDispatcher; + return dispatcher.dispatchEvent(request).then((res) => { + if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { + return Promise.reject(new OptimizelyError(FAILED_TO_DISPATCH_EVENTS, res.statusCode)); + } + return Promise.resolve(res); + }); + } + + private dispatchBatch(batch: EventBatch, closing: boolean): void { + const { request, events } = batch; + + events.forEach((event) => { + this.dispatchingEvents.set(event.id, event); + }); + + const runResult: RunResult<EventDispatcherResponse> = this.retryConfig + ? runWithRetry( + () => this.executeDispatch(request, closing), this.retryConfig.backoffProvider(), this.retryConfig.maxRetries + ) : { + result: this.executeDispatch(request, closing), + cancelRetry: () => {}, + }; + + this.eventEmitter.emit('dispatch', request); + + const taskId = this.idGenerator.getId(); + this.runningTask.set(taskId, runResult); + + runResult.result.then((res) => { + events.forEach((event) => { + this.eventStore?.remove(event.id); + if (!event.notStored && this.eventCountInStore) { + this.eventCountInStore--; + } + }); + return Promise.resolve(); + }).catch((err) => { + // if the dispatch fails, the events will still be + // in the store for future processing + this.logger?.error(err); + }).finally(() => { + this.runningTask.delete(taskId); + events.forEach((event) => this.dispatchingEvents.delete(event.id)); + }); + } + + private async flush(useClosingDispatcher = false): Promise<void> { + const batch = this.createNewBatch(); + if (!batch) { + return; + } + + this.dispatchRepeater.reset(); + this.dispatchBatch(batch, useClosingDispatcher); + } + + async process(event: ProcessableEvent): Promise<void> { + if (!this.isRunning()) { + return Promise.reject(new OptimizelyError(SERVICE_NOT_RUNNING, 'BatchEventProcessor')); + } + + const eventWithId: EventWithId = { + id: this.idGenerator.getId(), + event: event, + }; + + await this.storeEvent(eventWithId); + + if (this.eventQueue.length > 0 && !areEventContextsEqual(this.eventQueue[0].event, event)) { + this.flush(); + } + + this.eventQueue.push(eventWithId); + + if (this.eventQueue.length == this.batchSize) { + this.flush(); + } else if (!this.dispatchRepeater.isRunning()) { + this.dispatchRepeater.start(); + } + } + + private async readEventCountInStore(store: Store<EventWithId>): Promise<void> { + if (this.eventCountInStore !== undefined) { + return; + } + + try { + const keys = await store.getKeys(); + this.eventCountInStore = keys.length; + } catch (e) { + this.logger?.error(e); + } + } + + private async findEventCountInStore(): Promise<unknown> { + if (this.eventStore && this.eventCountInStore === undefined) { + const store = this.eventStore; + this.eventCountWaitPromise = this.eventCountWaitPromise.then(() => this.readEventCountInStore(store)); + return this.eventCountWaitPromise; + } + return Promise.resolve(); + } + + private async storeEvent(eventWithId: EventWithId): Promise<void> { + await this.findEventCountInStore(); + if (this.eventCountInStore !== undefined && this.eventCountInStore >= this.maxEventsInStore) { + this.logger?.info(EVENT_STORE_FULL, eventWithId.event.uuid); + eventWithId.notStored = true; + return; + } + + await Promise.resolve(this.eventStore?.set(eventWithId.id, eventWithId)).then(() => { + if (this.eventCountInStore !== undefined) { + this.eventCountInStore++; + } + }).catch((e) => { + eventWithId.notStored = true; + this.logger?.error(e); + }); + } + + start(): void { + if (!this.isNew()) { + return; + } + + super.start(); + this.state = ServiceState.Running; + + if(!this.disposable) { + this.failedEventRepeater?.start(); + } + + this.retryFailedEvents(); + this.startPromise.resolve(); + } + + makeDisposable(): void { + super.makeDisposable(); + this.batchSize = 1; + this.retryConfig = { + maxRetries: Math.min(this.retryConfig?.maxRetries ?? 5, 5), + backoffProvider: + this.retryConfig?.backoffProvider || + (() => new ExponentialBackoff(DEFAULT_MIN_BACKOFF, DEFAULT_MAX_BACKOFF, 500)), + } + } + + flushImmediately(): Promise<unknown> { + if (!this.isRunning()) { + return Promise.resolve(); + } + return this.flush(true); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (this.isNew()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'BatchEventProcessor') + )); + } + + this.state = ServiceState.Stopping; + this.dispatchRepeater.stop(); + this.failedEventRepeater?.stop(); + + this.flush(true); + this.runningTask.forEach((task) => task.cancelRetry()); + + Promise.allSettled(Array.from(this.runningTask.values()).map((task) => task.result)).then(() => { + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + }); + } +} diff --git a/lib/event_processor/event_builder/log_event.spec.ts b/lib/event_processor/event_builder/log_event.spec.ts new file mode 100644 index 000000000..ad3b22b94 --- /dev/null +++ b/lib/event_processor/event_builder/log_event.spec.ts @@ -0,0 +1,870 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from 'vitest'; + +import { + makeEventBatch, + buildLogEvent, +} from './log_event'; + +import { ImpressionEvent, ConversionEvent, UserEvent } from './user_event'; +import { Region } from '../../project_config/project_config'; + + +describe('makeEventBatch', () => { + it('should build a batch with single impression event when experiment and variation are defined', () => { + const impressionEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } + + const result = makeEventBatch([impressionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: 'layerId', + experiment_id: 'expId', + variation_id: 'varId', + metadata: { + flag_key: 'flagKey1', + rule_key: 'expKey', + rule_type: 'experiment', + variation_key: 'varKey', + enabled: true, + }, + }, + ], + events: [ + { + entity_id: 'layerId', + timestamp: 69, + key: 'campaign_activated', + uuid: 'uuid', + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should build a batch with simlge impression event when experiment and variation are not defined', () => { + const impressionEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: null, + }, + + experiment: { + id: null, + key: '', + }, + + variation: { + id: null, + key: '', + }, + + ruleKey: '', + flagKey: 'flagKey1', + ruleType: 'rollout', + enabled: true, + } + + const result = makeEventBatch([impressionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: null, + experiment_id: "", + variation_id: "", + metadata: { + flag_key: 'flagKey1', + rule_key: '', + rule_type: 'rollout', + variation_key: '', + enabled: true, + }, + }, + ], + events: [ + { + entity_id: null, + timestamp: 69, + key: 'campaign_activated', + uuid: 'uuid', + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }); + + it('should build a batch with single conversion event when tags object is defined', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } + + const result = makeEventBatch([conversionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should build a batch with single conversion event when when tags object is undefined', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: undefined, + + revenue: 1000, + value: 123, + } + + const result = makeEventBatch([conversionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: undefined, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should build a batch with single conversion event when event id is null', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: null, + key: 'event-key', + }, + + tags: undefined, + + revenue: 1000, + value: 123, + } + + const result = makeEventBatch([conversionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: null, + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: undefined, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should include revenue and value for conversion events if they are 0', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: 0, + revenue: 0, + }, + + revenue: 0, + value: 0, + } + + const result = makeEventBatch([conversionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: 0, + revenue: 0, + }, + revenue: 0, + value: 0, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should not include $opt_bot_filtering attribute if context.botFiltering is undefined', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } + + const result = makeEventBatch([conversionEvent]) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + ], + }, + ], + }) + }) + + it('should batch Conversion and Impression events together', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } + + const impressionEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } + + const result = makeEventBatch([impressionEvent, conversionEvent]) + + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: 'layerId', + experiment_id: 'expId', + variation_id: 'varId', + metadata: { + flag_key: 'flagKey1', + rule_key: 'expKey', + rule_type: 'experiment', + variation_key: 'varKey', + enabled: true, + }, + }, + ], + events: [ + { + entity_id: 'layerId', + timestamp: 69, + key: 'campaign_activated', + uuid: 'uuid', + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) +}) + +describe('buildLogEvent', () => { + it('should select the correct URL based on the event context region', () => { + const baseEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true + }, + user: { + id: 'userId', + attributes: [] + }, + layer: { + id: 'layerId' + }, + experiment: { + id: 'expId', + key: 'expKey' + }, + variation: { + id: 'varId', + key: 'varKey' + }, + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true + }; + + // Test for US region + const usEvent = { + ...baseEvent, + context: { + ...baseEvent.context, + region: 'US' as Region + } + }; + + const usResult = buildLogEvent([usEvent]); + expect(usResult.url).toBe('https://logx.optimizely.com/v1/events'); + + // Test for EU region + const euEvent = { + ...baseEvent, + context: { + ...baseEvent.context, + region: 'EU' as Region + } + }; + + const euResult = buildLogEvent([euEvent]); + expect(euResult.url).toBe('https://eu.logx.optimizely.com/v1/events'); + }); +}); diff --git a/lib/event_processor/event_builder/log_event.ts b/lib/event_processor/event_builder/log_event.ts new file mode 100644 index 000000000..4d4048950 --- /dev/null +++ b/lib/event_processor/event_builder/log_event.ts @@ -0,0 +1,232 @@ +/** + * Copyright 2021-2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConversionEvent, ImpressionEvent, UserEvent } from './user_event'; + +import { CONTROL_ATTRIBUTES } from '../../utils/enums'; + +import { LogEvent } from '../event_dispatcher/event_dispatcher'; +import { EventTags } from '../../shared_types'; +import { Region } from '../../project_config/project_config'; + +const ACTIVATE_EVENT_KEY = 'campaign_activated' +const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' + +export const logxEndpoint: Record<Region, string> = { + US: 'https://logx.optimizely.com/v1/events', + EU: 'https://eu.logx.optimizely.com/v1/events', +} + +export type EventBatch = { + account_id: string + project_id: string + revision: string + client_name: string + client_version: string + anonymize_ip: boolean + enrich_decisions: boolean + visitors: Visitor[] +} + +type Visitor = { + snapshots: Snapshot[] + visitor_id: string + attributes: Attribute[] +} + +type AttributeType = 'custom' + +export type Attribute = { + // attribute id + entity_id: string + // attribute key + key: string + type: AttributeType + value: string | number | boolean +} + +export type Snapshot = { + decisions?: Decision[] + events: SnapshotEvent[] +} + +type Decision = { + campaign_id: string | null + experiment_id: string | null + variation_id: string | null + metadata: Metadata +} + +type Metadata = { + flag_key: string; + rule_key: string; + rule_type: string; + variation_key: string; + enabled: boolean; + cmab_uuid?: string; +} + +export type SnapshotEvent = { + entity_id: string | null + timestamp: number + uuid: string + key: string + revenue?: number + value?: number + tags?: EventTags +} + +/** + * Given an array of batchable Decision or ConversionEvent events it returns + * a single EventV1 with proper batching + * + * @param {UserEvent[]} events + * @returns {EventBatch} + */ +export function makeEventBatch(events: UserEvent[]): EventBatch { + const visitors: Visitor[] = [] + const data = events[0] + + events.forEach(event => { + if (event.type === 'conversion' || event.type === 'impression') { + const visitor = makeVisitor(event) + + if (event.type === 'impression') { + visitor.snapshots.push(makeDecisionSnapshot(event)) + } else if (event.type === 'conversion') { + visitor.snapshots.push(makeConversionSnapshot(event)) + } + + visitors.push(visitor) + } + }) + + return { + client_name: data.context.clientName, + client_version: data.context.clientVersion, + + account_id: data.context.accountId, + project_id: data.context.projectId, + revision: data.context.revision, + anonymize_ip: data.context.anonymizeIP, + enrich_decisions: true, + + visitors, + } +} + +function makeConversionSnapshot(conversion: ConversionEvent): Snapshot { + const tags: EventTags = { + ...conversion.tags, + } + + delete tags['revenue'] + delete tags['value'] + + const event: SnapshotEvent = { + entity_id: conversion.event.id, + key: conversion.event.key, + timestamp: conversion.timestamp, + uuid: conversion.uuid, + } + + if (conversion.tags) { + event.tags = conversion.tags + } + + if (conversion.value != null) { + event.value = conversion.value + } + + if (conversion.revenue != null) { + event.revenue = conversion.revenue + } + + return { + events: [event], + } +} + +function makeDecisionSnapshot(event: ImpressionEvent): Snapshot { + const { layer, experiment, variation, ruleKey, flagKey, ruleType, enabled, cmabUuid } = event + const layerId = layer ? layer.id : null + const experimentId = experiment?.id ?? '' + const variationId = variation?.id ?? '' + const variationKey = variation ? variation.key : '' + + return { + decisions: [ + { + campaign_id: layerId, + experiment_id: experimentId, + variation_id: variationId, + metadata: { + flag_key: flagKey, + rule_key: ruleKey, + rule_type: ruleType, + variation_key: variationKey, + enabled: enabled, + cmab_uuid: cmabUuid, + }, + }, + ], + events: [ + { + entity_id: layerId, + timestamp: event.timestamp, + key: ACTIVATE_EVENT_KEY, + uuid: event.uuid, + }, + ], + } +} + +function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { + const visitor: Visitor = { + snapshots: [], + visitor_id: data.user.id, + attributes: [], + } + + data.user.attributes.forEach(attr => { + visitor.attributes.push({ + entity_id: attr.entityId, + key: attr.key, + type: 'custom' as const, // tell the compiler this is always string "custom" + value: attr.value, + }) + }) + + if (typeof data.context.botFiltering === 'boolean') { + visitor.attributes.push({ + entity_id: CONTROL_ATTRIBUTES.BOT_FILTERING, + key: CONTROL_ATTRIBUTES.BOT_FILTERING, + type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, + value: data.context.botFiltering, + }) + } + return visitor +} + +export function buildLogEvent(events: UserEvent[]): LogEvent { + const region = events[0]?.context.region || 'US'; + const url = logxEndpoint[region] || logxEndpoint['US']; + + return { + url, + httpVerb: 'POST', + params: makeEventBatch(events), + } +} diff --git a/lib/event_processor/event_builder/user_event.spec.ts b/lib/event_processor/event_builder/user_event.spec.ts new file mode 100644 index 000000000..e8cb373b3 --- /dev/null +++ b/lib/event_processor/event_builder/user_event.spec.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi } from 'vitest'; +import { buildImpressionEvent, buildConversionEvent } from './user_event'; +import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; +import { DecisionObj } from '../../core/decision_service'; +import testData from '../../tests/test_data'; + +describe('buildImpressionEvent', () => { + it('should use correct region from projectConfig in event context', () => { + const projectConfig = createProjectConfig( + testData.getTestProjectConfig(), + ) + + const experiment = projectConfig.experiments[0]; + const variation = experiment.variations[0]; + + const decisionObj = { + experiment, + variation, + decisionSource: 'experiment', + } as DecisionObj; + + + const impressionEvent = buildImpressionEvent({ + configObj: projectConfig, + decisionObj, + userId: 'test_user', + flagKey: 'test_flag', + enabled: true, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(impressionEvent.context.region).toBe('US'); + + projectConfig.region = 'EU'; + + const impressionEventEU = buildImpressionEvent({ + configObj: projectConfig, + decisionObj, + userId: 'test_user', + flagKey: 'test_flag', + enabled: true, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(impressionEventEU.context.region).toBe('EU'); + }); +}); + +describe('buildConversionEvent', () => { + it('should use correct region from projectConfig in event context', () => { + const projectConfig = createProjectConfig( + testData.getTestProjectConfig(), + ) + + const conversionEvent = buildConversionEvent({ + configObj: projectConfig, + userId: 'test_user', + eventKey: 'test_event', + eventTags: { revenue: 1000 }, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(conversionEvent.context.region).toBe('US'); + + projectConfig.region = 'EU'; + + const conversionEventEU = buildConversionEvent({ + configObj: projectConfig, + userId: 'test_user', + eventKey: 'test_event', + eventTags: { revenue: 1000 }, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(conversionEventEU.context.region).toBe('EU'); + }); +}); diff --git a/lib/event_processor/event_builder/user_event.tests.js b/lib/event_processor/event_builder/user_event.tests.js new file mode 100644 index 000000000..30f271d0e --- /dev/null +++ b/lib/event_processor/event_builder/user_event.tests.js @@ -0,0 +1,380 @@ +/** + * Copyright 2019-2020, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sinon from 'sinon'; +import { assert } from 'chai'; + +import fns from '../../utils/fns'; +import * as projectConfig from '../../project_config/project_config'; +import * as decision from '../../core/decision'; +import { buildImpressionEvent, buildConversionEvent } from './user_event'; + +describe('user_event', function() { + var configObj; + + beforeEach(function() { + configObj = { + region: 'US', + accountId: 'accountId', + projectId: 'projectId', + revision: '69', + anonymizeIP: true, + botFiltering: true, + }; + + sinon.stub(projectConfig, 'getEventId'); + sinon.stub(projectConfig, 'getLayerId'); + sinon.stub(projectConfig, 'getAttributeId'); + + sinon.stub(decision, 'getExperimentId'); + + sinon.stub(fns, 'uuid').returns('uuid'); + sinon.stub(fns, 'currentTimestamp').returns(100); + }); + + afterEach(function() { + decision.getExperimentId.restore(); + + projectConfig.getEventId.restore(); + projectConfig.getLayerId.restore(); + projectConfig.getAttributeId.restore(); + + fns.uuid.restore(); + fns.currentTimestamp.restore(); + }); + + describe('buildImpressionEvent', function() { + describe('when botFiltering and anonymizeIP are true', function() { + it('should build an ImpressionEvent with the correct attributes', function() { + var decisionObj = { + experiment: { + key: 'exp1', + status: 'Running', + forcedVariations: {}, + audienceIds: [], + layerId: 'layer-id', + trafficAllocation: [], + variationKeyMap: { + 'variation': { + key: 'var1', + id: 'var1-id', + } + }, + id: 'exp1-id', + variations: [{ key: 'var1', id: 'var1-id' }], + }, + variation: { + key: 'var1', + id: 'var1-id', + }, + decisionSource: 'experiment', + } + decision.getExperimentId.withArgs(decisionObj).returns('exp1-id'); + + projectConfig.getLayerId.withArgs(configObj, 'exp1-id').returns('layer-id'); + + projectConfig.getAttributeId.withArgs(configObj, 'plan_type').returns('plan_type_id'); + + + var result = buildImpressionEvent({ + configObj: configObj, + decisionObj: decisionObj, + enabled: true, + flagKey: 'flagkey1', + userId: 'user1', + userAttributes: { + plan_type: 'bronze', + invalid: 'value', + }, + clientEngine: 'node', + clientVersion: '3.0.11', + }); + + assert.deepEqual(result, { + type: 'impression', + timestamp: 100, + uuid: 'uuid', + context: { + region: 'US', + accountId: 'accountId', + projectId: 'projectId', + revision: '69', + clientName: 'node', + clientVersion: '3.0.11', + anonymizeIP: true, + botFiltering: true, + }, + + user: { + id: 'user1', + attributes: [ + { + entityId: 'plan_type_id', + key: 'plan_type', + value: 'bronze', + }, + ], + }, + + layer: { + id: 'layer-id', + }, + experiment: { + id: 'exp1-id', + key: 'exp1', + }, + variation: { + id: 'var1-id', + key: 'var1', + }, + + ruleKey: "exp1", + flagKey: 'flagkey1', + ruleType: 'experiment', + enabled: true, + cmabUuid: undefined, + }); + }); + }); + + describe('when botFiltering and anonymizeIP are undefined', function() { + it('should create an ImpressionEvent with the correct attributes', function() { + var decisionObj = { + experiment: { + key: 'exp1', + status: 'Running', + forcedVariations: {}, + audienceIds: [], + layerId: '253442', + trafficAllocation: [], + variationKeyMap: { + 'variation': { + key: 'var1', + id: 'var1-id', + } + }, + id: '1237847778', + variations: [{ key: 'var1', id: 'var1-id' }], + }, + variation: { + key: 'var1', + id: 'var1-id', + }, + decisionSource: 'experiment', + } + decision.getExperimentId.withArgs(decisionObj).returns('exp1-id'); + + projectConfig.getLayerId.withArgs(configObj, 'exp1-id').returns('layer-id'); + + projectConfig.getAttributeId.withArgs(configObj, 'plan_type').returns('plan_type_id'); + + delete configObj['anonymizeIP']; + delete configObj['botFiltering']; + + var result = buildImpressionEvent({ + configObj: configObj, + decisionObj: decisionObj, + flagKey: 'flagkey1', + enabled: false, + userId: 'user1', + userAttributes: { + plan_type: 'bronze', + invalid: 'value', + }, + clientEngine: 'node', + clientVersion: '3.0.11', + }); + + assert.deepEqual(result, { + type: 'impression', + timestamp: 100, + uuid: 'uuid', + context: { + region: 'US', + accountId: 'accountId', + projectId: 'projectId', + revision: '69', + clientName: 'node', + clientVersion: '3.0.11', + anonymizeIP: false, + botFiltering: undefined, + }, + + user: { + id: 'user1', + attributes: [ + { + entityId: 'plan_type_id', + key: 'plan_type', + value: 'bronze', + }, + ], + }, + + layer: { + id: 'layer-id', + }, + experiment: { + id: 'exp1-id', + key: 'exp1', + }, + variation: { + id: 'var1-id', + key: 'var1', + }, + + ruleKey: "exp1", + flagKey: 'flagkey1', + ruleType: 'experiment', + enabled: false, + cmabUuid: undefined, + }); + }); + }); + }); + + describe('buildConversionEvent', function() { + describe('when botFiltering and anonymizeIP are true', function() { + it('should build an ConversionEvent with the correct attributes', function() { + projectConfig.getEventId.withArgs(configObj, 'event').returns('event-id'); + projectConfig.getAttributeId.withArgs(configObj, 'plan_type').returns('plan_type_id'); + + var result = buildConversionEvent({ + configObj: configObj, + eventKey: 'event', + eventTags: { + value: '123', + revenue: 1000, + tag1: 'value1', + }, + userId: 'user1', + userAttributes: { + plan_type: 'bronze', + invalid: 'value', + }, + clientEngine: 'node', + clientVersion: '3.0.11', + }); + + assert.deepEqual(result, { + type: 'conversion', + timestamp: 100, + uuid: 'uuid', + context: { + region: 'US', + accountId: 'accountId', + projectId: 'projectId', + revision: '69', + clientName: 'node', + clientVersion: '3.0.11', + anonymizeIP: true, + botFiltering: true, + }, + + user: { + id: 'user1', + attributes: [ + { + entityId: 'plan_type_id', + key: 'plan_type', + value: 'bronze', + }, + ], + }, + + event: { + id: 'event-id', + key: 'event', + }, + + revenue: 1000, + value: 123, + tags: { + value: '123', + revenue: 1000, + tag1: 'value1', + }, + }); + }); + }); + + describe('when botFiltering and anonymizeIP are undefined', function() { + it('should create an ImpressionEvent with the correct attributes', function() { + projectConfig.getEventId.withArgs(configObj, 'event').returns('event-id'); + projectConfig.getAttributeId.withArgs(configObj, 'plan_type').returns('plan_type_id'); + + delete configObj['anonymizeIP']; + delete configObj['botFiltering']; + + var result = buildConversionEvent({ + configObj: configObj, + eventKey: 'event', + eventTags: { + value: '123', + revenue: 1000, + tag1: 'value1', + }, + userId: 'user1', + userAttributes: { + plan_type: 'bronze', + invalid: 'value', + }, + clientEngine: 'node', + clientVersion: '3.0.11', + }); + + assert.deepEqual(result, { + type: 'conversion', + timestamp: 100, + uuid: 'uuid', + context: { + region: 'US', + accountId: 'accountId', + projectId: 'projectId', + revision: '69', + clientName: 'node', + clientVersion: '3.0.11', + anonymizeIP: false, + botFiltering: undefined, + }, + + user: { + id: 'user1', + attributes: [ + { + entityId: 'plan_type_id', + key: 'plan_type', + value: 'bronze', + }, + ], + }, + + event: { + id: 'event-id', + key: 'event', + }, + + revenue: 1000, + value: 123, + tags: { + value: '123', + revenue: 1000, + tag1: 'value1', + }, + }); + }); + }); + }); +}); diff --git a/lib/event_processor/event_builder/user_event.ts b/lib/event_processor/event_builder/user_event.ts new file mode 100644 index 000000000..ae33d65da --- /dev/null +++ b/lib/event_processor/event_builder/user_event.ts @@ -0,0 +1,306 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DecisionObj } from '../../core/decision_service'; +import * as decision from '../../core/decision'; +import { isAttributeValid } from '../../utils/attributes_validator'; +import * as eventTagUtils from '../../utils/event_tag_utils'; +import fns from '../../utils/fns'; +import { + getAttributeId, + getEventId, + getLayerId, + ProjectConfig, + Region, +} from '../../project_config/project_config'; + +import { EventTags, UserAttributes } from '../../shared_types'; +import { LoggerFacade } from '../../logging/logger'; +import { DECISION_SOURCES } from '../../common_exports'; + +export type VisitorAttribute = { + entityId: string + key: string + value: string | number | boolean +} + +type EventContext = { + region?: Region; + accountId: string; + projectId: string; + revision: string; + clientName: string; + clientVersion: string; + anonymizeIP: boolean; + botFiltering?: boolean; +} + +type EventType = 'impression' | 'conversion'; + + +export type BaseUserEvent<T extends EventType> = { + type: T; + timestamp: number; + uuid: string; + context: EventContext; + user: { + id: string; + attributes: VisitorAttribute[]; + }; +}; + +export type ImpressionEvent = BaseUserEvent<'impression'> & { + layer: { + id: string | null; + } | null; + + experiment: { + id: string | null; + key: string; + } | null; + + variation: { + id: string | null; + key: string; + } | null; + + ruleKey: string; + flagKey: string; + ruleType: string; + enabled: boolean; + cmabUuid?: string; +}; + +export type ConversionEvent = BaseUserEvent<'conversion'> & { + event: { + id: string | null; + key: string; + } + + revenue: number | null; + value: number | null; + tags?: EventTags; +} + +export type UserEvent = ImpressionEvent | ConversionEvent; + +export const areEventContextsEqual = (eventA: UserEvent, eventB: UserEvent): boolean => { + const contextA = eventA.context + const contextB = eventB.context + + const regionA: Region = contextA.region || 'US'; + const regionB: Region = contextB.region || 'US'; + + return ( + regionA === regionB && + contextA.accountId === contextB.accountId && + contextA.projectId === contextB.projectId && + contextA.clientName === contextB.clientName && + contextA.clientVersion === contextB.clientVersion && + contextA.revision === contextB.revision && + contextA.anonymizeIP === contextB.anonymizeIP && + contextA.botFiltering === contextB.botFiltering + ) +} + +const buildBaseEvent = <T extends EventType>({ + configObj, + userId, + userAttributes, + clientEngine, + clientVersion, + type, +}: { + configObj: ProjectConfig; + userId: string; + userAttributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + type: T; +}): BaseUserEvent<T> => { + return { + type, + timestamp: fns.currentTimestamp(), + uuid: fns.uuid(), + context: { + region: configObj.region, + accountId: configObj.accountId, + projectId: configObj.projectId, + revision: configObj.revision, + clientName: clientEngine, + clientVersion: clientVersion, + anonymizeIP: configObj.anonymizeIP || false, + botFiltering: configObj.botFiltering, + }, + user: { + id: userId, + attributes: buildVisitorAttributes(configObj, userAttributes), + }, + }; + +} + +export type ImpressionConfig = { + decisionObj: DecisionObj; + userId: string; + flagKey: string; + enabled: boolean; + userAttributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; +} + +/** + * Creates an ImpressionEvent object from decision data + * @param {ImpressionConfig} config + * @return {ImpressionEvent} an ImpressionEvent object + */ +export const buildImpressionEvent = function({ + configObj, + decisionObj, + userId, + flagKey, + enabled, + userAttributes, + clientEngine, + clientVersion, +}: ImpressionConfig): ImpressionEvent { + + const ruleType = decisionObj.decisionSource; + const experimentKey = decision.getExperimentKey(decisionObj); + const experimentId = decision.getExperimentId(decisionObj); + const variationKey = decision.getVariationKey(decisionObj); + const variationId = decision.getVariationId(decisionObj); + const cmabUuid = decisionObj.cmabUuid; + const layerId = + experimentId !== null ? (ruleType === DECISION_SOURCES.HOLDOUT ? '' : getLayerId(configObj, experimentId)) : null; + + return { + ...buildBaseEvent({ + configObj, + userId, + userAttributes, + clientEngine, + clientVersion, + type: 'impression', + }), + + layer: { + id: layerId, + }, + + experiment: { + id: experimentId, + key: experimentKey, + }, + + variation: { + id: variationId, + key: variationKey, + }, + + ruleKey: experimentKey, + flagKey: flagKey, + ruleType: ruleType, + enabled: enabled, + cmabUuid, + }; +}; + +export type ConversionConfig = { + eventKey: string; + eventTags?: EventTags; + userId: string; + userAttributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; +} + +/** + * Creates a ConversionEvent object from track + * @param {ConversionConfig} config + * @return {ConversionEvent} a ConversionEvent object + */ +export const buildConversionEvent = function({ + configObj, + userId, + userAttributes, + clientEngine, + clientVersion, + eventKey, + eventTags, +}: ConversionConfig, logger?: LoggerFacade): ConversionEvent { + + const eventId = getEventId(configObj, eventKey); + + const revenue = eventTags ? eventTagUtils.getRevenueValue(eventTags, logger) : null; + const eventValue = eventTags ? eventTagUtils.getEventValue(eventTags, logger) : null; + + return { + ...buildBaseEvent({ + configObj, + userId, + userAttributes, + clientEngine, + clientVersion, + type: 'conversion', + }), + + event: { + id: eventId, + key: eventKey, + }, + + revenue: revenue, + value: eventValue, + tags: eventTags, + }; +}; + + +const buildVisitorAttributes = ( + configObj: ProjectConfig, + attributes?: UserAttributes, + logger?: LoggerFacade +): VisitorAttribute[] => { + if (!attributes) { + return []; + } + + // Omit attribute values that are not supported by the log endpoint. + const builtAttributes: VisitorAttribute[] = []; + Object.keys(attributes).forEach(function(attributeKey) { + const attributeValue = attributes[attributeKey]; + + if (typeof attributeValue === 'object' || typeof attributeValue === 'undefined') { + return; + } + + if (isAttributeValid(attributeKey, attributeValue)) { + const attributeId = getAttributeId(configObj, attributeKey, logger); + if (attributeId) { + builtAttributes.push({ + entityId: attributeId, + key: attributeKey, + value: attributeValue, + }); + } + } + }); + + return builtAttributes; +} diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.browser.spec.ts b/lib/event_processor/event_dispatcher/default_dispatcher.browser.spec.ts new file mode 100644 index 000000000..82314cbc7 --- /dev/null +++ b/lib/event_processor/event_dispatcher/default_dispatcher.browser.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, expect, it, describe, afterAll } from 'vitest'; + +vi.mock('./default_dispatcher', () => { + const DefaultEventDispatcher = vi.fn(); + return { DefaultEventDispatcher }; +}); + +vi.mock('../../utils/http_request_handler/request_handler.browser', () => { + const BrowserRequestHandler = vi.fn(); + return { BrowserRequestHandler }; +}); + +import { DefaultEventDispatcher } from './default_dispatcher'; +import { BrowserRequestHandler } from '../../utils/http_request_handler/request_handler.browser'; +import eventDispatcher from './default_dispatcher.browser'; + +describe('eventDispatcher', () => { + afterAll(() => { + MockDefaultEventDispatcher.mockReset(); + MockBrowserRequestHandler.mockReset(); + }); + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const MockDefaultEventDispatcher = vi.mocked(DefaultEventDispatcher); + + it('creates and returns the instance by calling DefaultEventDispatcher', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + }); + + it('uses a BrowserRequestHandler', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + expect(Object.is(MockDefaultEventDispatcher.mock.calls[0][0], MockBrowserRequestHandler.mock.instances[0])).toBe(true); + }); +}); diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts b/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts new file mode 100644 index 000000000..d38d266aa --- /dev/null +++ b/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserRequestHandler } from "../../utils/http_request_handler/request_handler.browser"; +import { EventDispatcher } from './event_dispatcher'; +import { DefaultEventDispatcher } from './default_dispatcher'; + +const eventDispatcher: EventDispatcher = new DefaultEventDispatcher(new BrowserRequestHandler()); + +export default eventDispatcher; diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.node.spec.ts b/lib/event_processor/event_dispatcher/default_dispatcher.node.spec.ts new file mode 100644 index 000000000..084fcce67 --- /dev/null +++ b/lib/event_processor/event_dispatcher/default_dispatcher.node.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vi, expect, it, describe, afterAll } from 'vitest'; + +vi.mock('./default_dispatcher', () => { + const DefaultEventDispatcher = vi.fn(); + return { DefaultEventDispatcher }; +}); + +vi.mock('../../utils/http_request_handler/request_handler.node', () => { + const NodeRequestHandler = vi.fn(); + return { NodeRequestHandler }; +}); + +import { DefaultEventDispatcher } from './default_dispatcher'; +import { NodeRequestHandler } from '../../utils/http_request_handler/request_handler.node'; +import eventDispatcher from './default_dispatcher.node'; + +describe('eventDispatcher', () => { + const MockNodeRequestHandler = vi.mocked(NodeRequestHandler); + const MockDefaultEventDispatcher = vi.mocked(DefaultEventDispatcher); + + afterAll(() => { + MockDefaultEventDispatcher.mockReset(); + MockNodeRequestHandler.mockReset(); + }) + + it('creates and returns the instance by calling DefaultEventDispatcher', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + }); + + it('uses a NodeRequestHandler', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + expect(Object.is(MockDefaultEventDispatcher.mock.calls[0][0], MockNodeRequestHandler.mock.instances[0])).toBe(true); + }); +}); diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.node.ts b/lib/event_processor/event_dispatcher/default_dispatcher.node.ts new file mode 100644 index 000000000..65dc115af --- /dev/null +++ b/lib/event_processor/event_dispatcher/default_dispatcher.node.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventDispatcher } from './event_dispatcher'; +import { NodeRequestHandler } from '../../utils/http_request_handler/request_handler.node'; +import { DefaultEventDispatcher } from './default_dispatcher'; + +const eventDispatcher: EventDispatcher = new DefaultEventDispatcher(new NodeRequestHandler()); + +export default eventDispatcher; diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.spec.ts b/lib/event_processor/event_dispatcher/default_dispatcher.spec.ts new file mode 100644 index 000000000..45a198788 --- /dev/null +++ b/lib/event_processor/event_dispatcher/default_dispatcher.spec.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, vi, describe, it } from 'vitest'; +import { DefaultEventDispatcher } from './default_dispatcher'; +import { EventBatch } from '../event_builder/log_event'; + +const getEvent = (): EventBatch => { + return { + account_id: 'string', + project_id: 'string', + revision: 'string', + client_name: 'string', + client_version: 'string', + anonymize_ip: true, + enrich_decisions: false, + visitors: [], + }; +}; + +describe('DefaultEventDispatcher', () => { + it('reject the response promise if the eventObj.httpVerb is not POST', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'GET' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.resolve({ statusCode: 203 }), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + await expect(dispatcher.dispatchEvent(eventObj)).rejects.toThrow(); + }); + + it('sends correct headers and data to the requestHandler', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'POST' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.resolve({ statusCode: 203 }), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + await dispatcher.dispatchEvent(eventObj); + + expect(requestHnadler.makeRequest).toHaveBeenCalledWith( + eventObj.url, + { + 'content-type': 'application/json' + }, + 'POST', + JSON.stringify(eventObj.params) + ); + }); + + it('returns a promise that resolves with correct value if the response of the requestHandler resolves', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'POST' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.resolve({ statusCode: 203 }), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + const response = await dispatcher.dispatchEvent(eventObj); + + expect(response.statusCode).toEqual(203); + }); + + it('returns a promise that rejects if the response of the requestHandler rejects', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'POST' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.reject(new Error('error')), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + await expect(dispatcher.dispatchEvent(eventObj)).rejects.toThrow(); + }); +}); diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.ts b/lib/event_processor/event_dispatcher/default_dispatcher.ts new file mode 100644 index 000000000..b786ffda2 --- /dev/null +++ b/lib/event_processor/event_dispatcher/default_dispatcher.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OptimizelyError } from '../../error/optimizly_error'; +import { ONLY_POST_REQUESTS_ARE_SUPPORTED } from 'error_message'; +import { RequestHandler } from '../../utils/http_request_handler/http'; +import { EventDispatcher, EventDispatcherResponse, LogEvent } from './event_dispatcher'; + +export class DefaultEventDispatcher implements EventDispatcher { + private requestHandler: RequestHandler; + + constructor(requestHandler: RequestHandler) { + this.requestHandler = requestHandler; + } + + async dispatchEvent( + eventObj: LogEvent + ): Promise<EventDispatcherResponse> { + // Non-POST requests not supported + if (eventObj.httpVerb !== 'POST') { + return Promise.reject(new OptimizelyError(ONLY_POST_REQUESTS_ARE_SUPPORTED)); + } + + const dataString = JSON.stringify(eventObj.params); + + const headers = { + 'content-type': 'application/json', + }; + + const abortableRequest = this.requestHandler.makeRequest(eventObj.url, headers, 'POST', dataString); + return abortableRequest.responsePromise; + } +} diff --git a/lib/event_processor/event_dispatcher/event_dispatcher.ts b/lib/event_processor/event_dispatcher/event_dispatcher.ts new file mode 100644 index 000000000..4dfda8f30 --- /dev/null +++ b/lib/event_processor/event_dispatcher/event_dispatcher.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventBatch } from "../event_builder/log_event"; + +export type EventDispatcherResponse = { + statusCode?: number +} + +export interface EventDispatcher { + dispatchEvent(event: LogEvent): Promise<EventDispatcherResponse> +} + +export interface LogEvent { + url: string + httpVerb: 'POST' | 'PUT' | 'GET' | 'PATCH' + params: EventBatch, +} diff --git a/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts new file mode 100644 index 000000000..383ad8380 --- /dev/null +++ b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestHandler } from '../../utils/http_request_handler/http'; +import { DefaultEventDispatcher } from './default_dispatcher'; +import { EventDispatcher } from './event_dispatcher'; + +import { validateRequestHandler } from '../../utils/http_request_handler/request_handler_validator'; + +export const createEventDispatcher = (requestHander: RequestHandler): EventDispatcher => { + validateRequestHandler(requestHander); + return new DefaultEventDispatcher(requestHander); +} diff --git a/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.spec.ts b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.spec.ts new file mode 100644 index 000000000..06bd5bd1f --- /dev/null +++ b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.spec.ts @@ -0,0 +1,81 @@ +/** + * Copyright 2023-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, beforeEach, it, expect, vi, MockInstance } from 'vitest'; + +import sendBeaconDispatcher, { Event } from './send_beacon_dispatcher.browser'; + +describe('dispatchEvent', function() { + let sendBeaconSpy: MockInstance<typeof navigator.sendBeacon>; + + beforeEach(() => { + sendBeaconSpy = vi.fn(); + navigator.sendBeacon = sendBeaconSpy as any; + }); + + it('should call sendBeacon with correct url, data and type', async () => { + const eventParams = { testParam: 'testParamValue' }; + const eventObj: Event = { + url: 'https://cdn.com/event', + httpVerb: 'POST', + params: eventParams, + }; + + sendBeaconSpy.mockReturnValue(true); + + sendBeaconDispatcher.dispatchEvent(eventObj) + + const [url, data] = sendBeaconSpy.mock.calls[0]; + const blob = data as Blob; + + const reader = new FileReader(); + reader.readAsBinaryString(blob); + + const sentParams = await new Promise((resolve) => { + reader.onload = () => { + resolve(reader.result); + }; + }); + + + expect(url).toEqual(eventObj.url); + expect(blob.type).toEqual('application/json'); + expect(sentParams).toEqual(JSON.stringify(eventObj.params)); + }); + + it('should resolve the response on sendBeacon success', async () => { + const eventParams = { testParam: 'testParamValue' }; + const eventObj: Event = { + url: 'https://cdn.com/event', + httpVerb: 'POST', + params: eventParams, + }; + + sendBeaconSpy.mockReturnValue(true); + await expect(sendBeaconDispatcher.dispatchEvent(eventObj)).resolves.not.toThrow(); + }); + + it('should reject the response on sendBeacon success', async () => { + const eventParams = { testParam: 'testParamValue' }; + const eventObj: Event = { + url: 'https://cdn.com/event', + httpVerb: 'POST', + params: eventParams, + }; + + sendBeaconSpy.mockReturnValue(false); + await expect(sendBeaconDispatcher.dispatchEvent(eventObj)).rejects.toThrow(); + }); +}); diff --git a/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts new file mode 100644 index 000000000..006adedd6 --- /dev/null +++ b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OptimizelyError } from '../../error/optimizly_error'; +import { SEND_BEACON_FAILED } from 'error_message'; +import { EventDispatcher, EventDispatcherResponse } from './event_dispatcher'; + +export type Event = { + url: string; + httpVerb: 'POST'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params: any; +} + +/** + * Sample event dispatcher implementation for tracking impression and conversions + * Users of the SDK can provide their own implementation + * @param {Event} eventObj + * @param {Function} callback + */ +export const dispatchEvent = function( + eventObj: Event, +): Promise<EventDispatcherResponse> { + const { params, url } = eventObj; + const blob = new Blob([JSON.stringify(params)], { + type: "application/json", + }); + + const success = navigator.sendBeacon(url, blob); + if(success) { + return Promise.resolve({}); + } + return Promise.reject(new OptimizelyError(SEND_BEACON_FAILED)); +} + +const eventDispatcher : EventDispatcher = { + dispatchEvent, +} + +export default eventDispatcher; diff --git a/lib/event_processor/event_processor.ts b/lib/event_processor/event_processor.ts new file mode 100644 index 000000000..585c71f68 --- /dev/null +++ b/lib/event_processor/event_processor.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2022-2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConversionEvent, ImpressionEvent } from './event_builder/user_event' +import { LogEvent } from './event_dispatcher/event_dispatcher' +import { Service } from '../service' +import { Consumer, Fn } from '../utils/type'; +import { LoggerFacade } from '../logging/logger'; + +export const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s +export const DEFAULT_BATCH_SIZE = 10 + +export type ProcessableEvent = ConversionEvent | ImpressionEvent + +export interface EventProcessor extends Service { + process(event: ProcessableEvent): Promise<unknown>; + onDispatch(handler: Consumer<LogEvent>): Fn; + setLogger(logger: LoggerFacade): void; + flushImmediately(): Promise<unknown>; +} diff --git a/lib/event_processor/event_processor_factory.browser.spec.ts b/lib/event_processor/event_processor_factory.browser.spec.ts new file mode 100644 index 000000000..a5d2a6af3 --- /dev/null +++ b/lib/event_processor/event_processor_factory.browser.spec.ts @@ -0,0 +1,179 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('./default_dispatcher.browser', () => { + return { default: {} }; +}); + +vi.mock('./event_processor_factory', async (importOriginal) => { + const getBatchEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const getOpaqueBatchEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const getForwardingEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const original: any = await importOriginal(); + return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor, getForwardingEventProcessor }; +}); + +vi.mock('../utils/cache/local_storage_cache.browser', () => { + return { LocalStorageCache: vi.fn() }; +}); + +vi.mock('../utils/cache/store', () => { + return { SyncPrefixStore: vi.fn() }; +}); + + +import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; +import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; +import { SyncPrefixStore } from '../utils/cache/store'; +import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor_factory.browser'; +import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser'; +import browserDefaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; +import { getOpaqueBatchEventProcessor } from './event_processor_factory'; + +describe('createForwardingEventProcessor', () => { + const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); + + beforeEach(() => { + mockGetForwardingEventProcessor.mockClear(); + }); + + it('returns forwarding event processor by calling getForwardingEventProcessor with the provided dispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = extractEventProcessor(createForwardingEventProcessor(eventDispatcher)); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher); + }); + + it('uses the browser default event dispatcher if none is provided', () => { + const processor = extractEventProcessor(createForwardingEventProcessor()); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, browserDefaultEventDispatcher); + }); +}); + +describe('createBatchEventProcessor', () => { + const mockGetOpaqueBatchEventProcessor = vi.mocked(getOpaqueBatchEventProcessor); + const MockLocalStorageCache = vi.mocked(LocalStorageCache); + const MockSyncPrefixStore = vi.mocked(SyncPrefixStore); + + beforeEach(() => { + mockGetOpaqueBatchEventProcessor.mockClear(); + MockLocalStorageCache.mockClear(); + MockSyncPrefixStore.mockClear(); + }); + + it('uses LocalStorageCache and SyncPrefixStore to create eventStore', () => { + const processor = createBatchEventProcessor({}); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + const eventStore = mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore; + expect(Object.is(eventStore, MockSyncPrefixStore.mock.results[0].value)).toBe(true); + + const [cache, prefix, transformGet, transformSet] = MockSyncPrefixStore.mock.calls[0]; + expect(Object.is(cache, MockLocalStorageCache.mock.results[0].value)).toBe(true); + expect(prefix).toBe(EVENT_STORE_PREFIX); + + // transformGet and transformSet should be identity functions + expect(transformGet('value')).toBe('value'); + expect(transformSet('value')).toBe('value'); + }); + + it('uses the provided eventDispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ eventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(eventDispatcher); + }); + + it('uses the default browser event dispatcher if none is provided', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(defaultEventDispatcher); + }); + + it('uses the provided closingEventDispatcher', () => { + const closingEventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ closingEventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(closingEventDispatcher); + }); + + it('does not use any closingEventDispatcher if eventDispatcher is provided but closingEventDispatcher is not', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ eventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(undefined); + }); + + it('uses the default sendBeacon event dispatcher if neither eventDispatcher nor closingEventDispatcher is provided', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(sendBeaconEventDispatcher); + }); + + it('uses the provided flushInterval', () => { + const processor1 = createBatchEventProcessor({ flushInterval: 2000 }); + expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].flushInterval).toBe(2000); + + const processor2 = createBatchEventProcessor({ }); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].flushInterval).toBe(undefined); + }); + + it('uses the provided batchSize', () => { + const processor1 = createBatchEventProcessor({ batchSize: 20 }); + expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].batchSize).toBe(20); + + const processor2 = createBatchEventProcessor({ }); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].batchSize).toBe(undefined); + }); + + it('uses maxRetries value of 5', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].retryOptions?.maxRetries).toBe(5); + }); + + it('uses the default failedEventRetryInterval', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].failedEventRetryInterval).toBe(FAILED_EVENT_RETRY_INTERVAL); + }); +}); diff --git a/lib/event_processor/event_processor_factory.browser.ts b/lib/event_processor/event_processor_factory.browser.ts new file mode 100644 index 000000000..e73b8bf24 --- /dev/null +++ b/lib/event_processor/event_processor_factory.browser.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventDispatcher } from './event_dispatcher/event_dispatcher'; +import { EventProcessor } from './event_processor'; +import { EventWithId } from './batch_event_processor'; +import { + getOpaqueBatchEventProcessor, + BatchEventProcessorOptions, + OpaqueEventProcessor, + wrapEventProcessor, + getForwardingEventProcessor, +} from './event_processor_factory'; +import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; +import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher.browser'; +import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; +import { SyncPrefixStore } from '../utils/cache/store'; +import { EVENT_STORE_PREFIX, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; + +export const DEFAULT_EVENT_BATCH_SIZE = 10; +export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher = defaultEventDispatcher, +): OpaqueEventProcessor => { + return wrapEventProcessor(getForwardingEventProcessor(eventDispatcher)); +}; + +const identity = <T>(v: T): T => v; + +export const createBatchEventProcessor = ( + options: BatchEventProcessorOptions = {} +): OpaqueEventProcessor => { + const localStorageCache = new LocalStorageCache<EventWithId>(); + const eventStore = new SyncPrefixStore<EventWithId, EventWithId>( + localStorageCache, EVENT_STORE_PREFIX, + identity, + identity, + ); + + return getOpaqueBatchEventProcessor({ + eventDispatcher: options.eventDispatcher || defaultEventDispatcher, + closingEventDispatcher: options.closingEventDispatcher || + (options.eventDispatcher ? undefined : sendBeaconEventDispatcher), + flushInterval: options.flushInterval, + batchSize: options.batchSize, + defaultFlushInterval: DEFAULT_EVENT_FLUSH_INTERVAL, + defaultBatchSize: DEFAULT_EVENT_BATCH_SIZE, + retryOptions: { + maxRetries: 5, + }, + failedEventRetryInterval: FAILED_EVENT_RETRY_INTERVAL, + eventStore, + }); +}; diff --git a/lib/event_processor/event_processor_factory.node.spec.ts b/lib/event_processor/event_processor_factory.node.spec.ts new file mode 100644 index 000000000..22b943f19 --- /dev/null +++ b/lib/event_processor/event_processor_factory.node.spec.ts @@ -0,0 +1,202 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('./default_dispatcher.node', () => { + return { default: {} }; +}); + +vi.mock('./event_processor_factory', async (importOriginal) => { + const getBatchEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const getOpaqueBatchEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); + const original: any = await importOriginal(); + return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor, getForwardingEventProcessor }; +}); + +vi.mock('../utils/cache/async_storage_cache.react_native', () => { + return { AsyncStorageCache: vi.fn() }; +}); + +vi.mock('../utils/cache/store', () => { + return { SyncPrefixStore: vi.fn(), AsyncPrefixStore: vi.fn() }; +}); + +import { createBatchEventProcessor, createForwardingEventProcessor } from './event_processor_factory.node'; +import nodeDefaultEventDispatcher from './event_dispatcher/default_dispatcher.node'; +import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import { getOpaqueBatchEventProcessor } from './event_processor_factory'; +import { AsyncStore, AsyncPrefixStore, SyncStore, SyncPrefixStore } from '../utils/cache/store'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; + +describe('createForwardingEventProcessor', () => { + const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); + + beforeEach(() => { + mockGetForwardingEventProcessor.mockClear(); + }); + + it('returns forwarding event processor by calling getForwardingEventProcessor with the provided dispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = extractEventProcessor(createForwardingEventProcessor(eventDispatcher)); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher); + }); + + it('uses the node default event dispatcher if none is provided', () => { + const processor = extractEventProcessor(createForwardingEventProcessor()); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, nodeDefaultEventDispatcher); + }); +}); + +describe('createBatchEventProcessor', () => { + const mockGetOpaqueBatchEventProcessor = vi.mocked(getOpaqueBatchEventProcessor); + const MockAsyncStorageCache = vi.mocked(AsyncStorageCache); + const MockSyncPrefixStore = vi.mocked(SyncPrefixStore); + const MockAsyncPrefixStore = vi.mocked(AsyncPrefixStore); + + beforeEach(() => { + mockGetOpaqueBatchEventProcessor.mockClear(); + MockAsyncStorageCache.mockClear(); + MockSyncPrefixStore.mockClear(); + MockAsyncPrefixStore.mockClear(); + }); + + it('uses no default event store if no eventStore is provided', () => { + const processor = createBatchEventProcessor({}); + + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + const eventStore = mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore; + expect(eventStore).toBe(undefined); + }); + + it('wraps the provided eventStore in a SyncPrefixStore if a SyncCache is provided as eventStore', () => { + const eventStore = { + operation: 'sync', + } as SyncStore<string>; + + const processor = createBatchEventProcessor({ eventStore }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore).toBe(MockSyncPrefixStore.mock.results[0].value); + const [cache, prefix, transformGet, transformSet] = MockSyncPrefixStore.mock.calls[0]; + + expect(cache).toBe(eventStore); + expect(prefix).toBe(EVENT_STORE_PREFIX); + + // transformGet and transformSet should be JSON.parse and JSON.stringify + expect(transformGet('{"value": 1}')).toEqual({ value: 1 }); + expect(transformSet({ value: 1 })).toBe('{"value":1}'); + }); + + it('wraps the provided eventStore in a AsyncPrefixStore if a AsyncCache is provided as eventStore', () => { + const eventStore = { + operation: 'async', + } as AsyncStore<string>; + + const processor = createBatchEventProcessor({ eventStore }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore).toBe(MockAsyncPrefixStore.mock.results[0].value); + const [cache, prefix, transformGet, transformSet] = MockAsyncPrefixStore.mock.calls[0]; + + expect(cache).toBe(eventStore); + expect(prefix).toBe(EVENT_STORE_PREFIX); + + // transformGet and transformSet should be JSON.parse and JSON.stringify + expect(transformGet('{"value": 1}')).toEqual({ value: 1 }); + expect(transformSet({ value: 1 })).toBe('{"value":1}'); + }); + + + it('uses the provided eventDispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ eventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(eventDispatcher); + }); + + it('uses the default node event dispatcher if none is provided', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(nodeDefaultEventDispatcher); + }); + + it('uses the provided closingEventDispatcher', () => { + const closingEventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ closingEventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(closingEventDispatcher); + + const processor2 = createBatchEventProcessor({ }); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].closingEventDispatcher).toBe(undefined); + }); + + it('uses the provided flushInterval', () => { + const processor1 = createBatchEventProcessor({ flushInterval: 2000 }); + expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].flushInterval).toBe(2000); + + const processor2 = createBatchEventProcessor({ }); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].flushInterval).toBe(undefined); + }); + + it('uses the provided batchSize', () => { + const processor1 = createBatchEventProcessor({ batchSize: 20 }); + expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].batchSize).toBe(20); + + const processor2 = createBatchEventProcessor({ }); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].batchSize).toBe(undefined); + }); + + it('uses maxRetries value of 5', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].retryOptions?.maxRetries).toBe(5); + }); + + it('uses no failed event retry if an eventStore is not provided', () => { + const processor = createBatchEventProcessor({ }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].failedEventRetryInterval).toBe(undefined); + }); + + it('uses the default failedEventRetryInterval if an eventStore is provided', () => { + const processor = createBatchEventProcessor({ eventStore: {} as any }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].failedEventRetryInterval).toBe(FAILED_EVENT_RETRY_INTERVAL); + }); +}); diff --git a/lib/event_processor/event_processor_factory.node.ts b/lib/event_processor/event_processor_factory.node.ts new file mode 100644 index 000000000..b0ed4ffde --- /dev/null +++ b/lib/event_processor/event_processor_factory.node.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventDispatcher } from './event_dispatcher/event_dispatcher'; +import defaultEventDispatcher from './event_dispatcher/default_dispatcher.node'; +import { + BatchEventProcessorOptions, + FAILED_EVENT_RETRY_INTERVAL, + getOpaqueBatchEventProcessor, + getPrefixEventStore, + OpaqueEventProcessor, + wrapEventProcessor, + getForwardingEventProcessor, +} from './event_processor_factory'; + +export const DEFAULT_EVENT_BATCH_SIZE = 10; +export const DEFAULT_EVENT_FLUSH_INTERVAL = 30_000; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher = defaultEventDispatcher, +): OpaqueEventProcessor => { + return wrapEventProcessor(getForwardingEventProcessor(eventDispatcher)); +}; + +export const createBatchEventProcessor = ( + options: BatchEventProcessorOptions = {} +): OpaqueEventProcessor => { + const eventStore = options.eventStore ? getPrefixEventStore(options.eventStore) : undefined; + + return getOpaqueBatchEventProcessor({ + eventDispatcher: options.eventDispatcher || defaultEventDispatcher, + closingEventDispatcher: options.closingEventDispatcher, + flushInterval: options.flushInterval, + batchSize: options.batchSize, + defaultFlushInterval: DEFAULT_EVENT_FLUSH_INTERVAL, + defaultBatchSize: DEFAULT_EVENT_BATCH_SIZE, + retryOptions: { + maxRetries: 5, + + }, + failedEventRetryInterval: eventStore ? FAILED_EVENT_RETRY_INTERVAL : undefined, + eventStore, + }); +}; diff --git a/lib/event_processor/event_processor_factory.react_native.spec.ts b/lib/event_processor/event_processor_factory.react_native.spec.ts new file mode 100644 index 000000000..630417a5e --- /dev/null +++ b/lib/event_processor/event_processor_factory.react_native.spec.ts @@ -0,0 +1,265 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('./default_dispatcher.browser', () => { + return { default: {} }; +}); + + +vi.mock('./event_processor_factory', async importOriginal => { + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); + const getBatchEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const getOpaqueBatchEventProcessor = vi.fn().mockImplementation(() => { + return {}; + }); + const original: any = await importOriginal(); + return { ...original, getBatchEventProcessor, getOpaqueBatchEventProcessor, getForwardingEventProcessor }; +}); + +vi.mock('../utils/cache/async_storage_cache.react_native', () => { + return { AsyncStorageCache: vi.fn() }; +}); + +vi.mock('../utils/cache/store', () => { + return { SyncPrefixStore: vi.fn(), AsyncPrefixStore: vi.fn() }; +}); + +vi.mock('@react-native-community/netinfo', () => { + return { NetInfoState: {}, addEventListener: vi.fn() }; +}); +let isAsyncStorageAvailable = true; + +await vi.hoisted(async () => { + await mockRequireNetInfo(); +}); + +async function mockRequireNetInfo() { + const { Module } = await import('module'); + const M: any = Module; + + M._load_original = M._load; + M._load = (uri: string, parent: string) => { + if (uri === '@react-native-async-storage/async-storage') { + if (isAsyncStorageAvailable) return {}; + throw new Error("Module not found: @react-native-async-storage/async-storage"); + } + return M._load_original(uri, parent); + }; +} + +import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor_factory.react_native'; +import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; +import { EVENT_STORE_PREFIX, extractEventProcessor, getForwardingEventProcessor, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import { getOpaqueBatchEventProcessor } from './event_processor_factory'; +import { AsyncStore, AsyncPrefixStore, SyncStore, SyncPrefixStore } from '../utils/cache/store'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; +import { MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE } from '../utils/import.react_native/@react-native-async-storage/async-storage'; + +describe('createForwardingEventProcessor', () => { + const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); + + beforeEach(() => { + mockGetForwardingEventProcessor.mockClear(); + }); + + it('returns forwarding event processor by calling getForwardingEventProcessor with the provided dispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = extractEventProcessor(createForwardingEventProcessor(eventDispatcher)); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher); + }); + + it('uses the browser default event dispatcher if none is provided', () => { + const processor = extractEventProcessor(createForwardingEventProcessor()); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, defaultEventDispatcher); + }); +}); + +describe('createBatchEventProcessor', () => { + const mockGetOpaqueBatchEventProcessor = vi.mocked(getOpaqueBatchEventProcessor); + const MockAsyncStorageCache = vi.mocked(AsyncStorageCache); + const MockSyncPrefixStore = vi.mocked(SyncPrefixStore); + const MockAsyncPrefixStore = vi.mocked(AsyncPrefixStore); + + beforeEach(() => { + mockGetOpaqueBatchEventProcessor.mockClear(); + MockAsyncStorageCache.mockClear(); + MockSyncPrefixStore.mockClear(); + MockAsyncPrefixStore.mockClear(); + }); + + it('uses AsyncStorageCache and AsyncPrefixStore to create eventStore if no eventStore is provided', () => { + const processor = createBatchEventProcessor({}); + + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + const eventStore = mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore; + expect(Object.is(eventStore, MockAsyncPrefixStore.mock.results[0].value)).toBe(true); + + const [cache, prefix, transformGet, transformSet] = MockAsyncPrefixStore.mock.calls[0]; + expect(Object.is(cache, MockAsyncStorageCache.mock.results[0].value)).toBe(true); + expect(prefix).toBe(EVENT_STORE_PREFIX); + + // transformGet and transformSet should be identity functions + expect(transformGet('value')).toBe('value'); + expect(transformSet('value')).toBe('value'); + }); + + it('should throw error if @react-native-async-storage/async-storage is not available', async () => { + isAsyncStorageAvailable = false; + const { AsyncStorageCache } = await vi.importActual< + typeof import('../utils/cache/async_storage_cache.react_native') + >('../utils/cache/async_storage_cache.react_native'); + + MockAsyncStorageCache.mockImplementationOnce(() => { + return new AsyncStorageCache(); + }); + + expect(() => createBatchEventProcessor({})).toThrowError( + MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE + ); + + isAsyncStorageAvailable = true; + }); + + it('should not throw error if eventStore is provided and @react-native-async-storage/async-storage is not available', async () => { + isAsyncStorageAvailable = false; + const eventStore = { + operation: 'sync', + } as SyncStore<string>; + + const { AsyncStorageCache } = await vi.importActual< + typeof import('../utils/cache/async_storage_cache.react_native') + >('../utils/cache/async_storage_cache.react_native'); + + MockAsyncStorageCache.mockImplementationOnce(() => { + return new AsyncStorageCache(); + }); + + expect(() => createBatchEventProcessor({ eventStore })).not.toThrow(); + + isAsyncStorageAvailable = true; + }); + + it('wraps the provided eventStore in a SyncPrefixStore if a SyncCache is provided as eventStore', () => { + const eventStore = { + operation: 'sync', + } as SyncStore<string>; + + const processor = createBatchEventProcessor({ eventStore }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore).toBe(MockSyncPrefixStore.mock.results[0].value); + const [cache, prefix, transformGet, transformSet] = MockSyncPrefixStore.mock.calls[0]; + + expect(cache).toBe(eventStore); + expect(prefix).toBe(EVENT_STORE_PREFIX); + + // transformGet and transformSet should be JSON.parse and JSON.stringify + expect(transformGet('{"value": 1}')).toEqual({ value: 1 }); + expect(transformSet({ value: 1 })).toBe('{"value":1}'); + }); + + it('wraps the provided eventStore in a AsyncPrefixStore if a AsyncCache is provided as eventStore', () => { + const eventStore = { + operation: 'async', + } as AsyncStore<string>; + + const processor = createBatchEventProcessor({ eventStore }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventStore).toBe(MockAsyncPrefixStore.mock.results[0].value); + const [cache, prefix, transformGet, transformSet] = MockAsyncPrefixStore.mock.calls[0]; + + expect(cache).toBe(eventStore); + expect(prefix).toBe(EVENT_STORE_PREFIX); + + // transformGet and transformSet should be JSON.parse and JSON.stringify + expect(transformGet('{"value": 1}')).toEqual({ value: 1 }); + expect(transformSet({ value: 1 })).toBe('{"value":1}'); + }); + + it('uses the provided eventDispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ eventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(eventDispatcher); + }); + + it('uses the default browser event dispatcher if none is provided', () => { + const processor = createBatchEventProcessor({}); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(defaultEventDispatcher); + }); + + it('uses the provided closingEventDispatcher', () => { + const closingEventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createBatchEventProcessor({ closingEventDispatcher }); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(closingEventDispatcher); + + const processor2 = createBatchEventProcessor({}); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].closingEventDispatcher).toBe(undefined); + }); + + it('uses the provided flushInterval', () => { + const processor1 = createBatchEventProcessor({ flushInterval: 2000 }); + expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].flushInterval).toBe(2000); + + const processor2 = createBatchEventProcessor({}); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].flushInterval).toBe(undefined); + }); + + it('uses the provided batchSize', () => { + const processor1 = createBatchEventProcessor({ batchSize: 20 }); + expect(Object.is(processor1, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].batchSize).toBe(20); + + const processor2 = createBatchEventProcessor({}); + expect(Object.is(processor2, mockGetOpaqueBatchEventProcessor.mock.results[1].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[1][0].batchSize).toBe(undefined); + }); + + it('uses maxRetries value of 5', () => { + const processor = createBatchEventProcessor({}); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].retryOptions?.maxRetries).toBe(5); + }); + + it('uses the default failedEventRetryInterval', () => { + const processor = createBatchEventProcessor({}); + expect(Object.is(processor, mockGetOpaqueBatchEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetOpaqueBatchEventProcessor.mock.calls[0][0].failedEventRetryInterval).toBe(FAILED_EVENT_RETRY_INTERVAL); + }); +}); diff --git a/lib/event_processor/event_processor_factory.react_native.ts b/lib/event_processor/event_processor_factory.react_native.ts new file mode 100644 index 000000000..b46b594a4 --- /dev/null +++ b/lib/event_processor/event_processor_factory.react_native.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventDispatcher } from './event_dispatcher/event_dispatcher'; +import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; +import { + BatchEventProcessorOptions, + getOpaqueBatchEventProcessor, + getPrefixEventStore, + OpaqueEventProcessor, + wrapEventProcessor, + getForwardingEventProcessor, +} from './event_processor_factory'; +import { EVENT_STORE_PREFIX, FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; +import { AsyncPrefixStore } from '../utils/cache/store'; +import { EventWithId } from './batch_event_processor'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; +import { ReactNativeNetInfoEventProcessor } from './batch_event_processor.react_native'; + +export const DEFAULT_EVENT_BATCH_SIZE = 10; +export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher = defaultEventDispatcher, +): OpaqueEventProcessor => { + return wrapEventProcessor(getForwardingEventProcessor(eventDispatcher)); +}; + +const identity = <T>(v: T): T => v; + +const getDefaultEventStore = () => { + const asyncStorageCache = new AsyncStorageCache<EventWithId>(); + + const eventStore = new AsyncPrefixStore<EventWithId, EventWithId>( + asyncStorageCache, + EVENT_STORE_PREFIX, + identity, + identity, + ); + + return eventStore; +} + +export const createBatchEventProcessor = ( + options: BatchEventProcessorOptions = {} +): OpaqueEventProcessor => { + const eventStore = options.eventStore ? getPrefixEventStore(options.eventStore) : getDefaultEventStore(); + + return getOpaqueBatchEventProcessor( + { + eventDispatcher: options.eventDispatcher || defaultEventDispatcher, + closingEventDispatcher: options.closingEventDispatcher, + flushInterval: options.flushInterval, + batchSize: options.batchSize, + defaultFlushInterval: DEFAULT_EVENT_FLUSH_INTERVAL, + defaultBatchSize: DEFAULT_EVENT_BATCH_SIZE, + retryOptions: { + maxRetries: 5, + }, + failedEventRetryInterval: FAILED_EVENT_RETRY_INTERVAL, + eventStore, + }, + ReactNativeNetInfoEventProcessor + ); +}; diff --git a/lib/event_processor/event_processor_factory.spec.ts b/lib/event_processor/event_processor_factory.spec.ts new file mode 100644 index 000000000..9aaa97f55 --- /dev/null +++ b/lib/event_processor/event_processor_factory.spec.ts @@ -0,0 +1,417 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach, vi, MockInstance } from 'vitest'; +import { getBatchEventProcessor } from './event_processor_factory'; +import { BatchEventProcessor, BatchEventProcessorConfig, EventWithId,DEFAULT_MAX_BACKOFF, DEFAULT_MIN_BACKOFF } from './batch_event_processor'; +import { ExponentialBackoff, IntervalRepeater } from '../utils/repeater/repeater'; +import { getMockSyncCache } from '../tests/mock/mock_cache'; +import { LogLevel } from '../logging/logger'; + +vi.mock('./batch_event_processor'); +vi.mock('../utils/repeater/repeater'); + +const getMockEventDispatcher = () => { + return { + dispatchEvent: vi.fn(), + } +}; + +describe('getBatchEventProcessor', () => { + const MockBatchEventProcessor = vi.mocked(BatchEventProcessor); + const MockExponentialBackoff = vi.mocked(ExponentialBackoff); + const MockIntervalRepeater = vi.mocked(IntervalRepeater); + + beforeEach(() => { + MockBatchEventProcessor.mockReset(); + MockExponentialBackoff.mockReset(); + MockIntervalRepeater.mockReset(); + }); + + it('should throw an error if provided eventDispatcher is not valid', () => { + expect(() => getBatchEventProcessor({ + eventDispatcher: undefined as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: null as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: 'abc' as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: {} as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: { dispatchEvent: 'abc' } as any, + defaultFlushInterval: 10000, + defaultBatchSize: 10, + })).toThrow('Invalid event dispatcher'); + }); + + it('should throw and error if provided event store is invalid', () => { + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: 'abc' as any, + })).toThrow('Invalid store'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: 123 as any, + })).toThrow('Invalid store'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: {} as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: 'abc', get: 'abc', remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + const noop = () => {}; + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: noop, get: 'abc' } as any, + })).toThrow('Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: noop, get: noop, remove: 'abc' } as any, + })).toThrow('Invalid store method remove, Invalid store method getKeys'); + + expect(() => getBatchEventProcessor({ + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 10000, + defaultBatchSize: 10, + eventStore: { set: noop, get: noop, remove: noop, getKeys: 'abc' } as any, + })).toThrow('Invalid store method getKeys'); + }); + + it('returns an instane of BatchEventProcessor if no subclass constructor is provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 1000, + defaultBatchSize: 10, + }; + + const processor = getBatchEventProcessor(options); + + expect(processor instanceof BatchEventProcessor).toBe(true); + }); + + it('returns an instane of the provided subclass constructor', () => { + class CustomEventProcessor extends BatchEventProcessor { + constructor(opts: BatchEventProcessorConfig) { + super(opts); + } + } + + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 1000, + defaultBatchSize: 10, + }; + + const processor = getBatchEventProcessor(options, CustomEventProcessor); + + expect(processor instanceof CustomEventProcessor).toBe(true); + }); + + it('does not use retry if retryOptions is not provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 1000, + defaultBatchSize: 10, + }; + + const processor = getBatchEventProcessor(options); + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].retryConfig).toBe(undefined); + }); + + it('uses the correct maxRetries value when retryOptions is provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 1000, + defaultBatchSize: 10, + retryOptions: { + maxRetries: 10, + }, + }; + + const processor = getBatchEventProcessor(options); + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].retryConfig?.maxRetries).toBe(10); + }); + + it('uses exponential backoff with default parameters when retryOptions is provided without backoff values', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 1000, + defaultBatchSize: 10, + retryOptions: { maxRetries: 2 }, + }; + + const processor = getBatchEventProcessor(options); + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + + expect(MockBatchEventProcessor.mock.calls[0][0].retryConfig?.maxRetries).toBe(2); + + const backoffProvider = MockBatchEventProcessor.mock.calls[0][0].retryConfig?.backoffProvider; + expect(backoffProvider).not.toBe(undefined); + const backoff = backoffProvider?.(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff).toHaveBeenNthCalledWith(1, DEFAULT_MIN_BACKOFF, DEFAULT_MAX_BACKOFF, 500); + }); + + it('uses exponential backoff with provided backoff values in retryOptions', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 1000, + defaultBatchSize: 10, + retryOptions: { maxRetries: 2, minBackoff: 1000, maxBackoff: 2000 }, + }; + + const processor = getBatchEventProcessor(options); + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + + expect(MockBatchEventProcessor.mock.calls[0][0].retryConfig?.maxRetries).toBe(2); + + const backoffProvider = MockBatchEventProcessor.mock.calls[0][0].retryConfig?.backoffProvider; + + expect(backoffProvider).not.toBe(undefined); + const backoff = backoffProvider?.(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff).toHaveBeenNthCalledWith(1, 1000, 2000, 500); + }); + + it('uses a IntervalRepeater with default flush interval and adds a startup log if flushInterval is not provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultFlushInterval: 12345, + defaultBatchSize: 77, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + const usedRepeater = MockBatchEventProcessor.mock.calls[0][0].dispatchRepeater; + expect(Object.is(usedRepeater, MockIntervalRepeater.mock.instances[0])).toBe(true); + expect(MockIntervalRepeater).toHaveBeenNthCalledWith(1, 12345); + + const startupLogs = MockBatchEventProcessor.mock.calls[0][0].startupLogs; + expect(startupLogs).toEqual(expect.arrayContaining([{ + level: LogLevel.Warn, + message: 'Invalid flushInterval %s, defaulting to %s', + params: [undefined, 12345], + }])); + }); + + it('uses default flush interval and adds a startup log if flushInterval is less than 1', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + flushInterval: -1, + defaultFlushInterval: 12345, + defaultBatchSize: 77, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + const usedRepeater = MockBatchEventProcessor.mock.calls[0][0].dispatchRepeater; + expect(Object.is(usedRepeater, MockIntervalRepeater.mock.instances[0])).toBe(true); + expect(MockIntervalRepeater).toHaveBeenNthCalledWith(1, 12345); + + const startupLogs = MockBatchEventProcessor.mock.calls[0][0].startupLogs; + expect(startupLogs).toEqual(expect.arrayContaining([{ + level: LogLevel.Warn, + message: 'Invalid flushInterval %s, defaulting to %s', + params: [-1, 12345], + }])); + }); + + it('uses a IntervalRepeater with provided flushInterval and adds no startup log if provided flushInterval is valid', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + flushInterval: 12345, + defaultFlushInterval: 1000, + defaultBatchSize: 77, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + const usedRepeater = MockBatchEventProcessor.mock.calls[0][0].dispatchRepeater; + expect(Object.is(usedRepeater, MockIntervalRepeater.mock.instances[0])).toBe(true); + expect(MockIntervalRepeater).toHaveBeenNthCalledWith(1, 12345); + + const startupLogs = MockBatchEventProcessor.mock.calls[0][0].startupLogs; + expect(startupLogs?.find((log) => log.message === 'Invalid flushInterval %s, defaulting to %s')).toBe(undefined); + }); + + + it('uses default batch size and adds a startup log if batchSize is not provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].batchSize).toBe(77); + + const startupLogs = MockBatchEventProcessor.mock.calls[0][0].startupLogs; + expect(startupLogs).toEqual(expect.arrayContaining([{ + level: LogLevel.Warn, + message: 'Invalid batchSize %s, defaulting to %s', + params: [undefined, 77], + }])); + }); + + it('uses default size and adds a startup log if provided batchSize is less than 1', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + batchSize: -1, + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].batchSize).toBe(77); + + const startupLogs = MockBatchEventProcessor.mock.calls[0][0].startupLogs; + expect(startupLogs).toEqual(expect.arrayContaining([{ + level: LogLevel.Warn, + message: 'Invalid batchSize %s, defaulting to %s', + params: [-1, 77], + }])); + }); + + it('does not use a failedEventRepeater if failedEventRetryInterval is not provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].failedEventRepeater).toBe(undefined); + }); + + it('uses a IntervalRepeater with provided failedEventRetryInterval as failedEventRepeater', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + failedEventRetryInterval: 12345, + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(Object.is(MockBatchEventProcessor.mock.calls[0][0].failedEventRepeater, MockIntervalRepeater.mock.instances[1])).toBe(true); + expect(MockIntervalRepeater).toHaveBeenNthCalledWith(2, 12345); + }); + + it('uses the provided eventDispatcher', () => { + const eventDispatcher = getMockEventDispatcher(); + const options = { + eventDispatcher, + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].eventDispatcher).toBe(eventDispatcher); + }); + + it('does not use any closingEventDispatcher if not provided', () => { + const options = { + eventDispatcher: getMockEventDispatcher(), + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(undefined); + }); + + it('uses the provided closingEventDispatcher', () => { + const closingEventDispatcher = getMockEventDispatcher(); + const options = { + eventDispatcher: getMockEventDispatcher(), + closingEventDispatcher, + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].closingEventDispatcher).toBe(closingEventDispatcher); + }); + + it('uses the provided eventStore', () => { + const eventStore = getMockSyncCache<EventWithId>(); + const options = { + eventDispatcher: getMockEventDispatcher(), + eventStore, + defaultBatchSize: 77, + defaultFlushInterval: 12345, + }; + + const processor = getBatchEventProcessor(options); + + expect(Object.is(processor, MockBatchEventProcessor.mock.instances[0])).toBe(true); + expect(MockBatchEventProcessor.mock.calls[0][0].eventStore).toBe(eventStore); + }); +}); diff --git a/lib/event_processor/event_processor_factory.ts b/lib/event_processor/event_processor_factory.ts new file mode 100644 index 000000000..393ce436a --- /dev/null +++ b/lib/event_processor/event_processor_factory.ts @@ -0,0 +1,175 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LogLevel } from "../logging/logger"; +import { StartupLog } from "../service"; +import { ExponentialBackoff, IntervalRepeater } from "../utils/repeater/repeater"; +import { EventDispatcher } from "./event_dispatcher/event_dispatcher"; +import { EventProcessor } from "./event_processor"; +import { ForwardingEventProcessor } from "./forwarding_event_processor"; +import { BatchEventProcessor, DEFAULT_MAX_BACKOFF, DEFAULT_MIN_BACKOFF, EventWithId, RetryConfig } from "./batch_event_processor"; +import { AsyncPrefixStore, Store, SyncPrefixStore } from "../utils/cache/store"; +import { Maybe } from "../utils/type"; +import { validateStore } from "../utils/cache/store_validator"; + +export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher'; + +export const FAILED_EVENT_RETRY_INTERVAL = 20 * 1000; +export const EVENT_STORE_PREFIX = 'optly_event:'; + +export const getPrefixEventStore = (store: Store<string>): Store<EventWithId> => { + if (store.operation === 'async') { + return new AsyncPrefixStore<string, EventWithId>( + store, + EVENT_STORE_PREFIX, + JSON.parse, + JSON.stringify, + ); + } else { + return new SyncPrefixStore<string, EventWithId>( + store, + EVENT_STORE_PREFIX, + JSON.parse, + JSON.stringify, + ); + } +}; + +const eventProcessorSymbol: unique symbol = Symbol(); + +export type OpaqueEventProcessor = { + [eventProcessorSymbol]: unknown; +}; + +export type BatchEventProcessorOptions = { + eventDispatcher?: EventDispatcher; + closingEventDispatcher?: EventDispatcher; + flushInterval?: number; + batchSize?: number; + eventStore?: Store<string>; +}; + +export type BatchEventProcessorFactoryOptions = Omit<BatchEventProcessorOptions, 'eventDispatcher' | 'eventStore' > & { + eventDispatcher: EventDispatcher; + closingEventDispatcher?: EventDispatcher; + failedEventRetryInterval?: number; + defaultFlushInterval: number; + defaultBatchSize: number; + eventStore?: Store<EventWithId>; + retryOptions?: { + maxRetries: number; + minBackoff?: number; + maxBackoff?: number; + }; +} + +export const validateEventDispatcher = (eventDispatcher: EventDispatcher): void => { + if (!eventDispatcher || typeof eventDispatcher !== 'object' || typeof eventDispatcher.dispatchEvent !== 'function') { + throw new Error(INVALID_EVENT_DISPATCHER); + } +} + +export const getBatchEventProcessor = ( + options: BatchEventProcessorFactoryOptions, + EventProcessorConstructor: typeof BatchEventProcessor = BatchEventProcessor + ): EventProcessor => { + const { eventDispatcher, closingEventDispatcher, retryOptions, eventStore } = options; + + validateEventDispatcher(eventDispatcher); + if (closingEventDispatcher) { + validateEventDispatcher(closingEventDispatcher); + } + + if (eventStore) { + validateStore(eventStore); + } + + const retryConfig: RetryConfig | undefined = retryOptions ? { + maxRetries: retryOptions.maxRetries, + backoffProvider: () => { + const minBackoff = retryOptions?.minBackoff ?? DEFAULT_MIN_BACKOFF; + const maxBackoff = retryOptions?.maxBackoff ?? DEFAULT_MAX_BACKOFF; + return new ExponentialBackoff(minBackoff, maxBackoff, 500); + } + } : undefined; + + const startupLogs: StartupLog[] = []; + + const { defaultFlushInterval, defaultBatchSize } = options; + + let flushInterval = defaultFlushInterval; + if (options.flushInterval === undefined || options.flushInterval <= 0) { + startupLogs.push({ + level: LogLevel.Warn, + message: 'Invalid flushInterval %s, defaulting to %s', + params: [options.flushInterval, defaultFlushInterval], + }); + } else { + flushInterval = options.flushInterval; + } + + let batchSize = defaultBatchSize; + if (options.batchSize === undefined || options.batchSize <= 0) { + startupLogs.push({ + level: LogLevel.Warn, + message: 'Invalid batchSize %s, defaulting to %s', + params: [options.batchSize, defaultBatchSize], + }); + } else { + batchSize = options.batchSize; + } + + const dispatchRepeater = new IntervalRepeater(flushInterval); + const failedEventRepeater = options.failedEventRetryInterval ? + new IntervalRepeater(options.failedEventRetryInterval) : undefined; + + return new EventProcessorConstructor({ + eventDispatcher, + closingEventDispatcher, + dispatchRepeater, + failedEventRepeater, + retryConfig, + batchSize, + eventStore, + startupLogs, + }); +} + +export const wrapEventProcessor = (eventProcessor: EventProcessor): OpaqueEventProcessor => { + return { + [eventProcessorSymbol]: eventProcessor, + }; +} + +export const getOpaqueBatchEventProcessor = ( + options: BatchEventProcessorFactoryOptions, + EventProcessorConstructor: typeof BatchEventProcessor = BatchEventProcessor +): OpaqueEventProcessor => { + return wrapEventProcessor(getBatchEventProcessor(options, EventProcessorConstructor)); +} + +export const extractEventProcessor = (eventProcessor: Maybe<OpaqueEventProcessor>): Maybe<EventProcessor> => { + if (!eventProcessor || typeof eventProcessor !== 'object') { + return undefined; + } + return eventProcessor[eventProcessorSymbol] as Maybe<EventProcessor>; +} + + +export function getForwardingEventProcessor(dispatcher: EventDispatcher): EventProcessor { + validateEventDispatcher(dispatcher); + return new ForwardingEventProcessor(dispatcher); +} diff --git a/lib/event_processor/event_processor_factory.universal.ts b/lib/event_processor/event_processor_factory.universal.ts new file mode 100644 index 000000000..0a3b2ec56 --- /dev/null +++ b/lib/event_processor/event_processor_factory.universal.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getForwardingEventProcessor } from './event_processor_factory'; +import { EventDispatcher } from './event_dispatcher/event_dispatcher'; + +import { + getOpaqueBatchEventProcessor, + BatchEventProcessorOptions, + OpaqueEventProcessor, + wrapEventProcessor, + getPrefixEventStore, +} from './event_processor_factory'; + +export const DEFAULT_EVENT_BATCH_SIZE = 10; +export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; + +import { FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher +): OpaqueEventProcessor => { + return wrapEventProcessor(getForwardingEventProcessor(eventDispatcher)); +}; + +export type UniversalBatchEventProcessorOptions = Omit<BatchEventProcessorOptions, 'eventDispatcher'> & { + eventDispatcher: EventDispatcher; +} + +export const createBatchEventProcessor = ( + options: UniversalBatchEventProcessorOptions +): OpaqueEventProcessor => { + const eventStore = options.eventStore ? getPrefixEventStore(options.eventStore) : undefined; + + return getOpaqueBatchEventProcessor({ + eventDispatcher: options.eventDispatcher, + closingEventDispatcher: options.closingEventDispatcher, + flushInterval: options.flushInterval, + batchSize: options.batchSize, + defaultFlushInterval: DEFAULT_EVENT_FLUSH_INTERVAL, + defaultBatchSize: DEFAULT_EVENT_BATCH_SIZE, + retryOptions: { + maxRetries: 5, + }, + failedEventRetryInterval: FAILED_EVENT_RETRY_INTERVAL, + eventStore: eventStore, + }); +}; diff --git a/lib/event_processor/forwarding_event_processor.spec.ts b/lib/event_processor/forwarding_event_processor.spec.ts new file mode 100644 index 000000000..65d571cb9 --- /dev/null +++ b/lib/event_processor/forwarding_event_processor.spec.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2021, 2024-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, describe, it, vi } from 'vitest'; + +import { EventDispatcher } from './event_dispatcher/event_dispatcher'; +import { buildLogEvent, makeEventBatch } from './event_builder/log_event'; +import { createImpressionEvent } from '../tests/mock/create_event'; +import { ServiceState } from '../service'; +import { ForwardingEventProcessor } from './forwarding_event_processor'; + +const getMockEventDispatcher = (): EventDispatcher => { + return { + dispatchEvent: vi.fn().mockResolvedValue({ statusCode: 200 }), + }; +}; + +describe('ForwardingEventProcessor', () => { + it('should resolve onRunning() when start is called', async () => { + const dispatcher = getMockEventDispatcher(); + + const processor = new ForwardingEventProcessor(dispatcher); + + processor.start(); + await expect(processor.onRunning()).resolves.not.toThrow(); + }); + + it('should dispatch event immediately when process is called', async() => { + const dispatcher = getMockEventDispatcher(); + const mockDispatch = vi.mocked(dispatcher.dispatchEvent); + + const processor = new ForwardingEventProcessor(dispatcher); + + processor.start(); + await processor.onRunning(); + + const event = createImpressionEvent(); + processor.process(event); + expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); + const data = mockDispatch.mock.calls[0][0].params; + expect(data).toEqual(makeEventBatch([event])); + }); + + it('should emit dispatch event when event is dispatched', async() => { + const dispatcher = getMockEventDispatcher(); + + const processor = new ForwardingEventProcessor(dispatcher); + + processor.start(); + await processor.onRunning(); + + const listener = vi.fn(); + processor.onDispatch(listener); + + const event = createImpressionEvent(); + processor.process(event); + expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); + expect(dispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent([event])); + expect(listener).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith(buildLogEvent([event])); + }); + + it('should remove dispatch listener when the function returned from onDispatch is called', async() => { + const dispatcher = getMockEventDispatcher(); + + const processor = new ForwardingEventProcessor(dispatcher); + + processor.start(); + await processor.onRunning(); + + const listener = vi.fn(); + const unsub = processor.onDispatch(listener); + + let event = createImpressionEvent(); + processor.process(event); + expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); + expect(dispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent([event])); + expect(listener).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith(buildLogEvent([event])); + + unsub(); + event = createImpressionEvent('id-a'); + processor.process(event); + expect(listener).toHaveBeenCalledOnce(); + }); + + it('should resolve onTerminated promise when stop is called', async () => { + const dispatcher = getMockEventDispatcher(); + const processor = new ForwardingEventProcessor(dispatcher); + processor.start(); + await processor.onRunning(); + + expect(processor.getState()).toEqual(ServiceState.Running); + + processor.stop(); + await expect(processor.onTerminated()).resolves.not.toThrow(); + }); + + it('should reject onRunning promise when stop is called in New state', async () => { + const dispatcher = getMockEventDispatcher(); + const processor = new ForwardingEventProcessor(dispatcher); + + expect(processor.getState()).toEqual(ServiceState.New); + + processor.stop(); + await expect(processor.onRunning()).rejects.toThrow(); + }); + }); diff --git a/lib/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts new file mode 100644 index 000000000..f578992c7 --- /dev/null +++ b/lib/event_processor/forwarding_event_processor.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2021-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { LogEvent } from './event_dispatcher/event_dispatcher'; +import { EventProcessor, ProcessableEvent } from './event_processor'; + +import { EventDispatcher } from '../shared_types'; +import { buildLogEvent } from './event_builder/log_event'; +import { BaseService, ServiceState } from '../service'; +import { EventEmitter } from '../utils/event_emitter/event_emitter'; +import { Consumer, Fn } from '../utils/type'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; +import { sprintf } from '../utils/fns'; + +export class ForwardingEventProcessor extends BaseService implements EventProcessor { + private dispatcher: EventDispatcher; + private eventEmitter: EventEmitter<{ dispatch: LogEvent }>; + + constructor(dispatcher: EventDispatcher) { + super(); + this.dispatcher = dispatcher; + this.eventEmitter = new EventEmitter(); + } + + process(event: ProcessableEvent): Promise<unknown> { + const formattedEvent = buildLogEvent([event]); + const res = this.dispatcher.dispatchEvent(formattedEvent); + this.eventEmitter.emit('dispatch', formattedEvent); + return res; + } + + start(): void { + if (!this.isNew()) { + return; + } + this.state = ServiceState.Running; + this.startPromise.resolve(); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (this.isNew()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'ForwardingEventProcessor')) + ); + } + + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + } + + onDispatch(handler: Consumer<LogEvent>): Fn { + return this.eventEmitter.on('dispatch', handler); + } + + flushImmediately(): Promise<unknown> { + return Promise.resolve(); + } +} diff --git a/lib/export_types.ts b/lib/export_types.ts new file mode 100644 index 000000000..ce795dbaa --- /dev/null +++ b/lib/export_types.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// config manager related types +export type { + StaticConfigManagerConfig, + PollingConfigManagerConfig, + OpaqueConfigManager, +} from './project_config/config_manager_factory'; + +// event processor related types +export type { + LogEvent, + EventDispatcherResponse, + EventDispatcher, +} from './event_processor/event_dispatcher/event_dispatcher'; + +export type { + BatchEventProcessorOptions, + OpaqueEventProcessor, +} from './event_processor/event_processor_factory'; + +// Odp manager related types +export type { + OdpManagerOptions, + OpaqueOdpManager, +} from './odp/odp_manager_factory'; + +export type { + UserAgentParser, +} from './odp/ua_parser/user_agent_parser'; + +// Vuid manager related types +export type { + VuidManagerOptions, + OpaqueVuidManager, +} from './vuid/vuid_manager_factory'; + +// Logger related types +export type { + LogHandler, +} from './logging/logger'; + +export type { + OpaqueLevelPreset, + LoggerConfig, + OpaqueLogger, +} from './logging/logger_factory'; + +// Error related types +export type { ErrorHandler } from './error/error_handler'; +export type { OpaqueErrorNotifier } from './error/error_notifier_factory'; + +export type { Cache } from './utils/cache/cache'; +export type { Store } from './utils/cache/store' + +export type { + NotificationType, + NotificationPayload, + ActivateListenerPayload as ActivateNotificationPayload, + DecisionListenerPayload as DecisionNotificationPayload, + TrackListenerPayload as TrackNotificationPayload, + LogEventListenerPayload as LogEventNotificationPayload, + OptimizelyConfigUpdateListenerPayload as OptimizelyConfigUpdateNotificationPayload, +} from './notification_center/type'; + +export type { + UserAttributeValue, + UserAttributes, + OptimizelyConfig, + FeatureVariableValue, + OptimizelyVariable, + OptimizelyVariation, + OptimizelyExperiment, + OptimizelyFeature, + OptimizelyDecisionContext, + OptimizelyForcedDecision, + EventTags, + Event, + DatafileOptions, + UserProfileService, + UserProfile, + ListenerPayload, + OptimizelyDecision, + OptimizelyUserContext, + Config, + Client, + ActivateListenerPayload, + TrackListenerPayload, + NotificationCenter, + OptimizelySegmentOption, +} from './shared_types'; diff --git a/lib/feature_toggle.ts b/lib/feature_toggle.ts new file mode 100644 index 000000000..54da8afff --- /dev/null +++ b/lib/feature_toggle.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +/** + * This module contains feature flags that control the availability of features under development. + * Each flag represents a feature that is not yet ready for production release. These flags + * serve multiple purposes in our development workflow: + * + * When a new feature is in development, it can be safely merged into the main branch + * while remaining disabled in production. This allows continuous integration without + * affecting the stability of production releases. The feature code will be automatically + * removed in production builds through tree-shaking when the flag is disabled. + * + * During development and testing, these flags can be easily mocked to enable/disable + * specific features. Once a feature is complete and ready for release, its corresponding + * flag and all associated checks can be removed from the codebase. + */ + +export const holdout = () => false as const; + +export type IfActive<T extends () => boolean, Y, N = unknown> = ReturnType<T> extends true ? Y : N; diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js new file mode 100644 index 000000000..8b2e93e7f --- /dev/null +++ b/lib/index.browser.tests.js @@ -0,0 +1,352 @@ +/** + * Copyright 2016-2020, 2022-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; +import sinon from 'sinon'; +import Optimizely from './optimizely'; +import testData from './tests/test_data'; +import packageJSON from '../package.json'; +import * as optimizelyFactory from './index.browser'; +import configValidator from './utils/config_validator'; +import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; +import { createProjectConfig } from './project_config/project_config'; +import { wrapConfigManager } from './project_config/config_manager_factory'; +import { wrapLogger } from './logging/logger_factory'; + +class MockLocalStorage { + store = {}; + + constructor() {} + + getItem(key) { + return this.store[key]; + } + + setItem(key, value) { + this.store[key] = value.toString(); + } + + clear() { + this.store = {}; + } + + removeItem(key) { + delete this.store[key]; + } +} + +if (!global.window) { + try { + global.window = { + localStorage: new MockLocalStorage(), + }; + } catch (e) { + console.error("Unable to overwrite global.window"); + } +} + +const pause = timeoutMilliseconds => { + return new Promise(resolve => setTimeout(resolve, timeoutMilliseconds)); +}; + +var getLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => getLogger(), +}) + +describe('javascript-sdk (Browser)', function() { + var clock; + + before(() => { + window.addEventListener = () => {}; + // sinon.spy(window, 'addEventListener') + }); + + beforeEach(function() { + clock = sinon.useFakeTimers(new Date()); + }); + + afterEach(function() { + clock.restore(); + }); + + describe('APIs', function() { + // it('should expose logger, errorHandler, eventDispatcher and enums', function() { + // assert.isDefined(optimizelyFactory.logging); + // assert.isDefined(optimizelyFactory.logging.createLogger); + // assert.isDefined(optimizelyFactory.logging.createNoOpLogger); + // assert.isDefined(optimizelyFactory.errorHandler); + // assert.isDefined(optimizelyFactory.eventDispatcher); + // assert.isDefined(optimizelyFactory.enums); + // }); + + describe('createInstance', function() { + var fakeErrorHandler = { handleError: function() {} }; + var fakeEventDispatcher = { dispatchEvent: function() {} }; + var mockLogger; + + beforeEach(function() { + mockLogger = getLogger(); + sinon.stub(mockLogger, 'error'); + + global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); + }); + + afterEach(function() { + mockLogger.error.restore(); + delete global.XMLHttpRequest; + }); + + + // TODO: pending event handling should be part of the event processor + // logic, not the dispatcher. Refactor accordingly. + // it('should invoke resendPendingEvents at most once', function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // errorHandler: fakeErrorHandler, + // logger: silentLogger, + // }); + + // sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); + + // optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // errorHandler: fakeErrorHandler, + // logger: silentLogger, + // }); + // optlyInstance.onReady().catch(function() {}); + + // sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); + // }); + + // it('should not throw if the provided config is not valid', function() { + // configValidator.validate.throws(new Error('INVALID_CONFIG_OR_SOMETHING')); + // assert.doesNotThrow(function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // logger: wrapLogger(mockLogger), + // }); + // }); + // }); + + it('should create an instance of optimizely', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + errorHandler: fakeErrorHandler, + logger: wrapLogger(mockLogger), + }); + + assert.instanceOf(optlyInstance, Optimizely); + assert.equal(optlyInstance.clientVersion, '6.1.0'); + }); + + it('should set the JavaScript client engine and version', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + errorHandler: fakeErrorHandler, + logger: wrapLogger(mockLogger), + }); + + assert.equal('javascript-sdk', optlyInstance.clientEngine); + assert.equal(packageJSON.version, optlyInstance.clientVersion); + }); + + it('should allow passing of "react-sdk" as the clientEngine', function() { + var optlyInstance = optimizelyFactory.createInstance({ + clientEngine: 'react-sdk', + projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + errorHandler: fakeErrorHandler, + logger: wrapLogger(mockLogger), + }); + assert.equal('react-sdk', optlyInstance.clientEngine); + }); + + it('should activate with provided event dispatcher', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + var activate = optlyInstance.activate('testExperiment', 'testUser'); + assert.strictEqual(activate, 'control'); + }); + + it('should be able to set and get a forced variation', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); + assert.strictEqual(didSetVariation, true); + + var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'control'); + }); + + it('should be able to set and unset a forced variation', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); + assert.strictEqual(didSetVariation, true); + + var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'control'); + + var didSetVariation2 = optlyInstance.setForcedVariation('testExperiment', 'testUser', null); + assert.strictEqual(didSetVariation2, true); + + var variation2 = optlyInstance.getForcedVariation('testExperiment', 'testUser'); + assert.strictEqual(variation2, null); + }); + + it('should be able to set multiple experiments for one user', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); + assert.strictEqual(didSetVariation, true); + + var didSetVariation2 = optlyInstance.setForcedVariation( + 'testExperimentLaunched', + 'testUser', + 'controlLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'control'); + + var variation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'testUser'); + assert.strictEqual(variation2, 'controlLaunched'); + }); + + it('should be able to set multiple experiments for one user, and unset one', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); + assert.strictEqual(didSetVariation, true); + + var didSetVariation2 = optlyInstance.setForcedVariation( + 'testExperimentLaunched', + 'testUser', + 'controlLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'testUser', null); + assert.strictEqual(didSetVariation2, true); + + var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'control'); + + var variation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'testUser'); + assert.strictEqual(variation2, null); + }); + + it('should be able to set multiple experiments for one user, and reset one', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); + assert.strictEqual(didSetVariation, true); + + var didSetVariation2 = optlyInstance.setForcedVariation( + 'testExperimentLaunched', + 'testUser', + 'controlLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + var didSetVariation2 = optlyInstance.setForcedVariation( + 'testExperimentLaunched', + 'testUser', + 'variationLaunched' + ); + assert.strictEqual(didSetVariation2, true); + + var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'control'); + + var variation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'testUser'); + assert.strictEqual(variation2, 'variationLaunched'); + }); + + it('should override bucketing when setForcedVariation is called', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); + assert.strictEqual(didSetVariation, true); + + var variation = optlyInstance.getVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'control'); + + var didSetVariation2 = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'variation'); + assert.strictEqual(didSetVariation2, true); + + var variation = optlyInstance.getVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'variation'); + }); + + it('should override bucketing when setForcedVariation is called for a not running experiment', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + })), + logger: wrapLogger(mockLogger), + }); + + var didSetVariation = optlyInstance.setForcedVariation( + 'testExperimentNotRunning', + 'testUser', + 'controlNotRunning' + ); + assert.strictEqual(didSetVariation, true); + + var variation = optlyInstance.getVariation('testExperimentNotRunning', 'testUser'); + assert.strictEqual(variation, null); + }); + }); + }); +}); diff --git a/lib/index.browser.ts b/lib/index.browser.ts new file mode 100644 index 000000000..0f644a844 --- /dev/null +++ b/lib/index.browser.ts @@ -0,0 +1,64 @@ +/** + * Copyright 2016-2017, 2019-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Config, Client } from './shared_types'; +import sendBeaconEventDispatcher from './event_processor/event_dispatcher/send_beacon_dispatcher.browser'; +import { getOptimizelyInstance } from './client_factory'; +import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; +import { JAVASCRIPT_CLIENT_ENGINE } from './utils/enums'; +import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; + +/** + * Creates an instance of the Optimizely class + * @param {Config} config + * @return {Client|null} the Optimizely client object + * null on error + */ +export const createInstance = function(config: Config): Client { + const client = getOptimizelyInstance({ + ...config, + requestHandler: new BrowserRequestHandler(), + }); + + if (client) { + const unloadEvent = 'onpagehide' in window ? 'pagehide' : 'unload'; + window.addEventListener( + unloadEvent, + () => { + client.flushImmediately(); + }, + ); + } + + return client; +}; + +export const getSendBeaconEventDispatcher = (): EventDispatcher | undefined => { + return sendBeaconEventDispatcher; +}; + +export { default as eventDispatcher } from './event_processor/event_dispatcher/default_dispatcher.browser'; + +export { createPollingProjectConfigManager } from './project_config/config_manager_factory.browser'; +export { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor/event_processor_factory.browser'; + +export { createOdpManager } from './odp/odp_manager_factory.browser'; +export { createVuidManager } from './vuid/vuid_manager_factory.browser'; + +export * from './common_exports'; + +export * from './export_types'; + +export const clientEngine: string = JAVASCRIPT_CLIENT_ENGINE; diff --git a/packages/optimizely-sdk/lib/index.browser.umdtests.js b/lib/index.browser.umdtests.js similarity index 84% rename from packages/optimizely-sdk/lib/index.browser.umdtests.js rename to lib/index.browser.umdtests.js index 32181928e..a13f5046b 100644 --- a/packages/optimizely-sdk/lib/index.browser.umdtests.js +++ b/lib/index.browser.umdtests.js @@ -1,5 +1,5 @@ /** - * Copyright 2018, Optimizely + * Copyright 2018-2020 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,22 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var configValidator = require('./utils/config_validator'); -var enums = require('./utils/enums'); -var logger = require('./plugins/logger'); - -var packageJSON = require('../package.json'); -var eventDispatcher = require('./plugins/event_dispatcher/index.browser'); -var testData = require('./tests/test_data'); - -var chai = require('chai'); -var assert = chai.assert; -var sinon = require('sinon'); +import { assert } from 'chai'; +import sinon from 'sinon'; + +import configValidator from './utils/config_validator'; +import * as enums from './utils/enums'; +import * as logger from './plugins/logger'; +import Optimizely from './optimizely'; +import testData from './tests/test_data'; +import packageJSON from '../package.json'; +import eventDispatcher from './plugins/event_dispatcher/index.browser'; +import { INVALID_CONFIG_OR_SOMETHING } from './exception_messages'; describe('javascript-sdk', function() { describe('APIs', function() { - var xhr; - var requests; describe('createInstance', function() { var fakeErrorHandler = { handleError: function() {} }; var fakeEventDispatcher = { dispatchEvent: function() {} }; @@ -40,18 +38,16 @@ describe('javascript-sdk', function() { logToConsole: false, }); sinon.stub(configValidator, 'validate'); + sinon.stub(Optimizely.prototype, 'close'); - xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - requests = []; - xhr.onCreate = function(req) { - requests.push(req); - }; + global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); sinon.spy(console, 'log'); sinon.spy(console, 'info'); sinon.spy(console, 'warn'); sinon.spy(console, 'error'); + + sinon.spy(window, 'addEventListener'); }); afterEach(function() { @@ -59,8 +55,10 @@ describe('javascript-sdk', function() { console.info.restore(); console.warn.restore(); console.error.restore(); + window.addEventListener.restore(); configValidator.validate.restore(); - xhr.restore(); + Optimizely.prototype.close.restore(); + delete global.XMLHttpRequest }); // this test has to come first due to local state of the logLevel @@ -68,12 +66,11 @@ describe('javascript-sdk', function() { // checking that INFO logs log for an unspecified logLevel var optlyInstance = window.optimizelySdk.createInstance({ datafile: testData.getTestProjectConfig(), - skipJSONValidation: true, }); assert.strictEqual(console.info.getCalls().length, 1); - call = console.info.getCalls()[0]; + var call = console.info.getCalls()[0]; assert.strictEqual(call.args.length, 1); - assert(call.args[0].indexOf('OPTIMIZELY: Skipping JSON schema validation.') > -1); + assert(call.args[0].indexOf('PROJECT_CONFIG: Skipping JSON schema validation.') > -1); }); it('should instantiate the logger with a custom logLevel when provided', function() { @@ -81,7 +78,6 @@ describe('javascript-sdk', function() { var optlyInstance = window.optimizelySdk.createInstance({ datafile: testData.getTestProjectConfig(), logLevel: enums.LOG_LEVEL.ERROR, - skipJSONValidation: true, }); assert.strictEqual(console.log.getCalls().length, 0); @@ -92,15 +88,19 @@ describe('javascript-sdk', function() { }); optlyInstance.activate('testExperiment', 'testUser'); assert.strictEqual(console.error.getCalls().length, 1); + // Invalid datafile causes onReady Promise rejection - catch this error + optlyInstanceInvalid.onReady().catch(function() {}); }); it('should not throw if the provided config is not valid', function() { - configValidator.validate.throws(new Error('Invalid config or something')); + configValidator.validate.throws(new Error(INVALID_CONFIG_OR_SOMETHING)); assert.doesNotThrow(function() { - window.optimizelySdk.createInstance({ + var optlyInstance = window.optimizelySdk.createInstance({ datafile: {}, logger: silentLogger, }); + // Invalid datafile causes onReady Promise rejection - catch this error + optlyInstance.onReady().catch(function() {}); }); }); @@ -113,6 +113,8 @@ describe('javascript-sdk', function() { }); assert.equal('javascript-sdk', optlyInstance.clientEngine); assert.equal(packageJSON.version, optlyInstance.clientVersion); + // Invalid datafile causes onReady Promise rejection - catch this error + optlyInstance.onReady().catch(function() {}); }); it('should activate with provided event dispatcher', function() { @@ -286,6 +288,18 @@ describe('javascript-sdk', function() { var variation = optlyInstance.getVariation('testExperimentNotRunning', 'testUser'); assert.strictEqual(variation, null); }); + + it('should hook into window `pagehide` event', function() { + var optlyInstance = window.optimizelySdk.createInstance({ + datafile: testData.getTestProjectConfig(), + errorHandler: fakeErrorHandler, + eventDispatcher: eventDispatcher, + logger: silentLogger, + }); + + sinon.assert.calledOnce(window.addEventListener); + sinon.assert.calledWith(window.addEventListener, sinon.match('pagehide').or(sinon.match('unload'))); + }); }); }); }); diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js new file mode 100644 index 000000000..343312174 --- /dev/null +++ b/lib/index.node.tests.js @@ -0,0 +1,213 @@ +/** + * Copyright 2016-2020, 2022-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; +import sinon from 'sinon'; + +import Optimizely from './optimizely'; +import testData from './tests/test_data'; +import * as optimizelyFactory from './index.node'; +import configValidator from './utils/config_validator'; +import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; +import { wrapConfigManager } from './project_config/config_manager_factory'; +import { wrapLogger } from './logging/logger_factory'; + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +describe('optimizelyFactory', function() { + describe('APIs', function() { + // it('should expose logger, errorHandler, eventDispatcher and enums', function() { + // assert.isDefined(optimizelyFactory.logging); + // assert.isDefined(optimizelyFactory.logging.createLogger); + // assert.isDefined(optimizelyFactory.logging.createNoOpLogger); + // assert.isDefined(optimizelyFactory.errorHandler); + // assert.isDefined(optimizelyFactory.eventDispatcher); + // assert.isDefined(optimizelyFactory.enums); + // }); + + describe('createInstance', function() { + var fakeErrorHandler = { handleError: function() {} }; + var fakeEventDispatcher = { dispatchEvent: function() {} }; + var fakeLogger = createLogger(); + + beforeEach(function() { + sinon.stub(fakeLogger, 'error'); + }); + + afterEach(function() { + fakeLogger.error.restore(); + }); + + // it('should not throw if the provided config is not valid and log an error if logger is passed in', function() { + // configValidator.validate.throws(new Error('Invalid config or something')); + // var localLogger = loggerPlugin.createLogger({ logLevel: enums.LOG_LEVEL.INFO }); + // assert.doesNotThrow(function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager(), + // logger: localLogger, + // }); + // }); + // sinon.assert.calledWith(localLogger.log, enums.LOG_LEVEL.ERROR); + // }); + + // it('should not throw if the provided config is not valid', function() { + // configValidator.validate.throws(new Error('INVALID_CONFIG_OR_SOMETHING')); + // assert.doesNotThrow(function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // logger: wrapLogger(fakeLogger), + // }); + // }); + // // sinon.assert.calledOnce(fakeLogger.error); + // }); + + it('should create an instance of optimizely', function() { + var optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + }); + + assert.instanceOf(optlyInstance, Optimizely); + assert.equal(optlyInstance.clientVersion, '6.1.0'); + }); + // TODO: user will create and inject an event processor + // these tests will be refactored accordingly + // describe('event processor configuration', function() { + // var eventProcessorSpy; + // beforeEach(function() { + // eventProcessorSpy = sinon.stub(eventProcessor, 'createEventProcessor').callThrough(); + // }); + + // afterEach(function() { + // eventProcessor.createEventProcessor.restore(); + // }); + + // it('should ignore invalid event flush interval and use default instead', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventFlushInterval: ['invalid', 'flush', 'interval'], + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // flushInterval: 30000, + // }) + // ); + // }); + + // it('should use default event flush interval when none is provided', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // flushInterval: 30000, + // }) + // ); + // }); + + // it('should use provided event flush interval when valid', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventFlushInterval: 10000, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // flushInterval: 10000, + // }) + // ); + // }); + + // it('should ignore invalid event batch size and use default instead', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventBatchSize: null, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // batchSize: 10, + // }) + // ); + // }); + + // it('should use default event batch size when none is provided', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // batchSize: 10, + // }) + // ); + // }); + + // it('should use provided event batch size when valid', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventBatchSize: 300, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // batchSize: 300, + // }) + // ); + // }); + // }); + }); + }); +}); diff --git a/lib/index.node.ts b/lib/index.node.ts new file mode 100644 index 000000000..02d162ed6 --- /dev/null +++ b/lib/index.node.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2016-2017, 2019-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { NODE_CLIENT_ENGINE } from './utils/enums'; +import { Client, Config } from './shared_types'; +import { getOptimizelyInstance, OptimizelyFactoryConfig } from './client_factory'; +import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; +import { NodeRequestHandler } from './utils/http_request_handler/request_handler.node'; + +/** + * Creates an instance of the Optimizely class + * @param {Config} config + * @return {Client|null} the Optimizely client object + * null on error + */ +export const createInstance = function(config: Config): Client { + const nodeConfig: OptimizelyFactoryConfig = { + ...config, + clientEngine: config.clientEngine || NODE_CLIENT_ENGINE, + requestHandler: new NodeRequestHandler(), + } + + return getOptimizelyInstance(nodeConfig); +}; + +export const getSendBeaconEventDispatcher = function(): EventDispatcher | undefined { + return undefined; +}; + +export { default as eventDispatcher } from './event_processor/event_dispatcher/default_dispatcher.node'; + +export { createPollingProjectConfigManager } from './project_config/config_manager_factory.node'; +export { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor/event_processor_factory.node'; + +export { createOdpManager } from './odp/odp_manager_factory.node'; +export { createVuidManager } from './vuid/vuid_manager_factory.node'; + +export * from './common_exports'; + +export * from './export_types'; + +export const clientEngine: string = NODE_CLIENT_ENGINE; diff --git a/lib/index.react_native.spec.ts b/lib/index.react_native.spec.ts new file mode 100644 index 000000000..3fa5a6357 --- /dev/null +++ b/lib/index.react_native.spec.ts @@ -0,0 +1,174 @@ +/** + * Copyright 2019-2020, 2022-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; + +import Optimizely from './optimizely'; +import testData from './tests/test_data'; +import packageJSON from '../package.json'; +import * as optimizelyFactory from './index.react_native'; +import configValidator from './utils/config_validator'; +import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; +import { createProjectConfig } from './project_config/project_config'; +import { getMockLogger } from './tests/mock/mock_logger'; +import { wrapConfigManager } from './project_config/config_manager_factory'; +import { wrapLogger } from './logging/logger_factory'; + +vi.mock('@react-native-community/netinfo'); +vi.mock('react-native-get-random-values') +vi.mock('fast-text-encoding') + +describe('javascript-sdk/react-native', () => { + beforeEach(() => { + vi.spyOn(optimizelyFactory.eventDispatcher, 'dispatchEvent'); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe('APIs', () => { + // it('should expose logger, errorHandler, eventDispatcher and enums', () => { + // expect(optimizelyFactory.eventDispatcher).toBeDefined(); + // expect(optimizelyFactory.enums).toBeDefined(); + // }); + + describe('createInstance', () => { + const fakeErrorHandler = { handleError: function() {} }; + const fakeEventDispatcher = { dispatchEvent: async function() { + return Promise.resolve({}); + } }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + let mockLogger; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + mockLogger = getMockLogger(); + vi.spyOn(console, 'error'); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + // it('should not throw if the provided config is not valid', () => { + // vi.spyOn(configValidator, 'validate').mockImplementation(() => { + // throw new Error('Invalid config or something'); + // }); + // expect(function() { + // const optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // logger: wrapLogger(mockLogger), + // }); + // }).not.toThrow(); + // }); + + it('should create an instance of optimizely', () => { + const optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // errorHandler: fakeErrorHandler, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // logger: mockLogger, + }); + + expect(optlyInstance).toBeInstanceOf(Optimizely); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(optlyInstance.clientVersion).toEqual('6.1.0'); + }); + + it('should set the React Native JS client engine and javascript SDK version', () => { + const optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: wrapConfigManager(getMockProjectConfigManager()), + // errorHandler: fakeErrorHandler, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // logger: mockLogger, + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect('react-native-js-sdk').toEqual(optlyInstance.clientEngine); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(packageJSON.version).toEqual(optlyInstance.clientVersion); + }); + + // it('should allow passing of "react-sdk" as the clientEngine and convert it to "react-native-sdk"', () => { + // const optlyInstance = optimizelyFactory.createInstance({ + // clientEngine: 'react-sdk', + // projectConfigManager: getMockProjectConfigManager(), + // errorHandler: fakeErrorHandler, + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // logger: mockLogger, + // }); + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // expect('react-native-sdk').toEqual(optlyInstance.clientEngine); + // }); + + // describe('when passing in logLevel', () => { + // beforeEach(() => { + // vi.spyOn(logging, 'setLogLevel'); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should call logging.setLogLevel', () => { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfig()), + // }), + // logLevel: optimizelyFactory.enums.LOG_LEVEL.ERROR, + // }); + // expect(logging.setLogLevel).toBeCalledTimes(1); + // expect(logging.setLogLevel).toBeCalledWith(optimizelyFactory.enums.LOG_LEVEL.ERROR); + // }); + // }); + + // describe('when passing in logger', () => { + // beforeEach(() => { + // vi.spyOn(logging, 'setLogHandler'); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should call logging.setLogHandler with the supplied logger', () => { + // const fakeLogger = { log: function() {} }; + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfig()), + // }), + // // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // // @ts-ignore + // logger: fakeLogger, + // }); + // expect(logging.setLogHandler).toBeCalledTimes(1); + // expect(logging.setLogHandler).toBeCalledWith(fakeLogger); + // }); + // }); + }); + }); +}); diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts new file mode 100644 index 000000000..c393261b7 --- /dev/null +++ b/lib/index.react_native.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2019-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import 'fast-text-encoding'; +import 'react-native-get-random-values'; + +import { Client, Config } from './shared_types'; +import { getOptimizelyInstance, OptimizelyFactoryConfig } from './client_factory'; +import { REACT_NATIVE_JS_CLIENT_ENGINE } from './utils/enums'; +import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; +import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; + +/** + * Creates an instance of the Optimizely class + * @param {Config} config + * @return {Client|null} the Optimizely client object + * null on error + */ +export const createInstance = function(config: Config): Client { + const rnConfig: OptimizelyFactoryConfig = { + ...config, + clientEngine: config.clientEngine || REACT_NATIVE_JS_CLIENT_ENGINE, + requestHandler: new BrowserRequestHandler(), + } + + return getOptimizelyInstance(rnConfig); +}; + +export const getSendBeaconEventDispatcher = function(): EventDispatcher | undefined { + return undefined; +}; + +export { default as eventDispatcher } from './event_processor/event_dispatcher/default_dispatcher.browser'; + +export { createPollingProjectConfigManager } from './project_config/config_manager_factory.react_native'; +export { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor/event_processor_factory.react_native'; + +export { createOdpManager } from './odp/odp_manager_factory.react_native'; +export { createVuidManager } from './vuid/vuid_manager_factory.react_native'; + +export * from './common_exports'; + +export * from './export_types'; + +export const clientEngine: string = REACT_NATIVE_JS_CLIENT_ENGINE; diff --git a/lib/index.universal.ts b/lib/index.universal.ts new file mode 100644 index 000000000..9aaaa8cd3 --- /dev/null +++ b/lib/index.universal.ts @@ -0,0 +1,136 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Client, Config } from './shared_types'; +import { getOptimizelyInstance } from './client_factory'; +import { JAVASCRIPT_CLIENT_ENGINE } from './utils/enums'; + +import { RequestHandler } from './utils/http_request_handler/http'; + +export type UniversalConfig = Config & { + requestHandler: RequestHandler; +} + +/** + * Creates an instance of the Optimizely class + * @param {Config} config + * @return {Client|null} the Optimizely client object + * null on error + */ +export const createInstance = function(config: UniversalConfig): Client { + return getOptimizelyInstance(config); +}; + +export { createEventDispatcher } from './event_processor/event_dispatcher/event_dispatcher_factory'; + +export { createPollingProjectConfigManager } from './project_config/config_manager_factory.universal'; + +export { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor/event_processor_factory.universal'; + +export { createOdpManager } from './odp/odp_manager_factory.universal'; + +// TODO: decide on vuid manager API for universal +// export { createVuidManager } from './vuid/vuid_manager_factory.node'; + +export * from './common_exports'; + +export const clientEngine: string = JAVASCRIPT_CLIENT_ENGINE; + +// type exports +export type { RequestHandler } from './utils/http_request_handler/http'; + +// config manager related types +export type { + StaticConfigManagerConfig, + OpaqueConfigManager, +} from './project_config/config_manager_factory'; + +export type { UniversalPollingConfigManagerConfig } from './project_config/config_manager_factory.universal'; + +// event processor related types +export type { + LogEvent, + EventDispatcherResponse, + EventDispatcher, +} from './event_processor/event_dispatcher/event_dispatcher'; + +export type { UniversalBatchEventProcessorOptions } from './event_processor/event_processor_factory.universal'; + +// odp manager related types +export type { + UniversalOdpManagerOptions, +} from './odp/odp_manager_factory.universal'; + +export type { + UserAgentParser, +} from './odp/ua_parser/user_agent_parser'; + +export type { + OpaqueEventProcessor, +} from './event_processor/event_processor_factory'; + +// Logger related types +export type { + LogHandler, +} from './logging/logger'; + +export type { + OpaqueLevelPreset, + LoggerConfig, + OpaqueLogger, +} from './logging/logger_factory'; + +// Error related types +export type { ErrorHandler } from './error/error_handler'; +export type { OpaqueErrorNotifier } from './error/error_notifier_factory'; + +export type { Cache } from './utils/cache/cache'; + +export type { + NotificationType, + NotificationPayload, + ActivateListenerPayload as ActivateNotificationPayload, + DecisionListenerPayload as DecisionNotificationPayload, + TrackListenerPayload as TrackNotificationPayload, + LogEventListenerPayload as LogEventNotificationPayload, + OptimizelyConfigUpdateListenerPayload as OptimizelyConfigUpdateNotificationPayload, +} from './notification_center/type'; + +export type { + UserAttributeValue, + UserAttributes, + OptimizelyConfig, + FeatureVariableValue, + OptimizelyVariable, + OptimizelyVariation, + OptimizelyExperiment, + OptimizelyFeature, + OptimizelyDecisionContext, + OptimizelyForcedDecision, + EventTags, + Event, + DatafileOptions, + UserProfileService, + UserProfile, + ListenerPayload, + OptimizelyDecision, + OptimizelyUserContext, + Config, + Client, + ActivateListenerPayload, + TrackListenerPayload, + NotificationCenter, + OptimizelySegmentOption, +} from './shared_types'; diff --git a/lib/logging/logger.spec.ts b/lib/logging/logger.spec.ts new file mode 100644 index 000000000..59edd3f96 --- /dev/null +++ b/lib/logging/logger.spec.ts @@ -0,0 +1,386 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, beforeEach, afterEach, it, expect, vi, afterAll } from 'vitest'; + +import { ConsoleLogHandler, LogLevel, OptimizelyLogger } from './logger'; +import { OptimizelyError } from '../error/optimizly_error'; + +describe('ConsoleLogHandler', () => { + const logSpy = vi.spyOn(console, 'log'); + const debugSpy = vi.spyOn(console, 'debug'); + const infoSpy = vi.spyOn(console, 'info'); + const warnSpy = vi.spyOn(console, 'warn'); + const errorSpy = vi.spyOn(console, 'error'); + + beforeEach(() => { + logSpy.mockClear(); + debugSpy.mockClear(); + infoSpy.mockClear(); + warnSpy.mockClear(); + vi.useFakeTimers().setSystemTime(0); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + afterAll(() => { + logSpy.mockRestore(); + debugSpy.mockRestore(); + infoSpy.mockRestore(); + warnSpy.mockRestore(); + errorSpy.mockRestore(); + + vi.useRealTimers(); + }); + + it('should call console.info for LogLevel.Info', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Info, 'test'); + + expect(infoSpy).toHaveBeenCalledTimes(1); + }); + + it('should call console.debug for LogLevel.Debug', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Debug, 'test'); + + expect(debugSpy).toHaveBeenCalledTimes(1); + }); + + + it('should call console.warn for LogLevel.Warn', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Warn, 'test'); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it('should call console.error for LogLevel.Error', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Error, 'test'); + + expect(errorSpy).toHaveBeenCalledTimes(1); + }); + + it('should format the log message', () => { + const logger = new ConsoleLogHandler(); + logger.log(LogLevel.Info, 'info message'); + logger.log(LogLevel.Debug, 'debug message'); + logger.log(LogLevel.Warn, 'warn message'); + logger.log(LogLevel.Error, 'error message'); + + expect(infoSpy).toHaveBeenCalledWith('[OPTIMIZELY] - INFO 1970-01-01T00:00:00.000Z info message'); + expect(debugSpy).toHaveBeenCalledWith('[OPTIMIZELY] - DEBUG 1970-01-01T00:00:00.000Z debug message'); + expect(warnSpy).toHaveBeenCalledWith('[OPTIMIZELY] - WARN 1970-01-01T00:00:00.000Z warn message'); + expect(errorSpy).toHaveBeenCalledWith('[OPTIMIZELY] - ERROR 1970-01-01T00:00:00.000Z error message'); + }); + + it('should use the prefix if provided', () => { + const logger = new ConsoleLogHandler('PREFIX'); + logger.log(LogLevel.Info, 'info message'); + logger.log(LogLevel.Debug, 'debug message'); + logger.log(LogLevel.Warn, 'warn message'); + logger.log(LogLevel.Error, 'error message'); + + expect(infoSpy).toHaveBeenCalledWith('PREFIX - INFO 1970-01-01T00:00:00.000Z info message'); + expect(debugSpy).toHaveBeenCalledWith('PREFIX - DEBUG 1970-01-01T00:00:00.000Z debug message'); + expect(warnSpy).toHaveBeenCalledWith('PREFIX - WARN 1970-01-01T00:00:00.000Z warn message'); + expect(errorSpy).toHaveBeenCalledWith('PREFIX - ERROR 1970-01-01T00:00:00.000Z error message'); + }); +}); + + +const mockMessageResolver = (prefix = '') => { + return { + resolve: vi.fn().mockImplementation((message) => `${prefix} ${message}`), + }; +} + +const mockLogHandler = () => { + return { + log: vi.fn(), + }; +} + +describe('OptimizelyLogger', () => { + it('should only log error when level is set to error', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + errorMsgResolver: messageResolver, + level: LogLevel.Error, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + }); + + it('should only log warn and error when level is set to warn', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + errorMsgResolver: messageResolver, + level: LogLevel.Warn, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log.mock.calls[1][0]).toBe(LogLevel.Warn); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + }); + + it('should only log info, warn and error when level is set to info', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: messageResolver, + errorMsgResolver: messageResolver, + level: LogLevel.Info, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log.mock.calls[1][0]).toBe(LogLevel.Warn); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(3); + expect(logHandler.log.mock.calls[2][0]).toBe(LogLevel.Info); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(3); + }); + + it('should log all levels when level is set to debug', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: messageResolver, + errorMsgResolver: messageResolver, + level: LogLevel.Debug, + }); + + logger.error('test'); + expect(logHandler.log).toHaveBeenCalledTimes(1); + expect(logHandler.log.mock.calls[0][0]).toBe(LogLevel.Error); + + logger.warn('test'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log.mock.calls[1][0]).toBe(LogLevel.Warn); + + logger.info('test'); + expect(logHandler.log).toHaveBeenCalledTimes(3); + expect(logHandler.log.mock.calls[2][0]).toBe(LogLevel.Info); + + logger.debug('test'); + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log.mock.calls[3][0]).toBe(LogLevel.Debug); + }); + + it('should skip logging debug/info levels if not infoMessageResolver is available', () => { + const logHandler = mockLogHandler(); + const messageResolver = mockMessageResolver(); + + const logger = new OptimizelyLogger({ + logHandler, + errorMsgResolver: messageResolver, + level: LogLevel.Debug, + }); + + logger.info('test'); + logger.debug('test'); + expect(logHandler.log).not.toHaveBeenCalled(); + }); + + it('should resolve debug/info messages using the infoMessageResolver', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.debug('msg one'); + logger.info('msg two'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Debug, 'info msg one'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Info, 'info msg two'); + }); + + it('should resolve warn/error messages using the infoMessageResolver', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.warn('msg one'); + logger.error('msg two'); + expect(logHandler.log).toHaveBeenCalledTimes(2); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Warn, 'err msg one'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Error, 'err msg two'); + }); + + it('should use the provided name as message prefix', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.warn('msg one'); + logger.error('msg two'); + logger.debug('msg three'); + logger.info('msg four'); + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Warn, 'EventManager: err msg one'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Error, 'EventManager: err msg two'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Debug, 'EventManager: info msg three'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Info, 'EventManager: info msg four'); + }); + + it('should format the message with the give parameters', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + logger.warn('msg %s, %s', 'one', 1); + logger.error('msg %s', 'two'); + logger.debug('msg three', 9999); + logger.info('msg four%s%s', '!', '!'); + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Warn, 'EventManager: err msg one, 1'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Error, 'EventManager: err msg two'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Debug, 'EventManager: info msg three'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Info, 'EventManager: info msg four!!'); + }); + + it('should log the message of the error object and ignore other arguments if first argument is an error object \ + other that OptimizelyError', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + logger.debug(new Error('msg debug %s'), 'a'); + logger.info(new Error('msg info %s'), 'b'); + logger.warn(new Error('msg warn %s'), 'c'); + logger.error(new Error('msg error %s'), 'd'); + + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Debug, 'EventManager: msg debug %s'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Info, 'EventManager: msg info %s'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Warn, 'EventManager: msg warn %s'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Error, 'EventManager: msg error %s'); + }); + + it('should resolve and log the message of an OptimizelyError using error resolver and ignore other arguments', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Debug, + }); + + const err = new OptimizelyError('msg %s %s', 1, 2); + logger.debug(err, 'a'); + logger.info(err, 'a'); + logger.warn(err, 'a'); + logger.error(err, 'a'); + + expect(logHandler.log).toHaveBeenCalledTimes(4); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Debug, 'EventManager: err msg 1 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Info, 'EventManager: err msg 1 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Warn, 'EventManager: err msg 1 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(4, LogLevel.Error, 'EventManager: err msg 1 2'); + }); + + it('should return a new logger with the new name but same level, handler and resolvers when child() is called', () => { + const logHandler = mockLogHandler(); + + const logger = new OptimizelyLogger({ + name: 'EventManager', + logHandler, + infoMsgResolver: mockMessageResolver('info'), + errorMsgResolver: mockMessageResolver('err'), + level: LogLevel.Info, + }); + + const childLogger = logger.child('ChildLogger'); + childLogger.debug('msg one %s', 1); + childLogger.info('msg two %s', 2); + childLogger.warn('msg three %s', 3); + childLogger.error('msg four %s', 4); + + expect(logHandler.log).toHaveBeenCalledTimes(3); + expect(logHandler.log).toHaveBeenNthCalledWith(1, LogLevel.Info, 'ChildLogger: info msg two 2'); + expect(logHandler.log).toHaveBeenNthCalledWith(2, LogLevel.Warn, 'ChildLogger: err msg three 3'); + expect(logHandler.log).toHaveBeenNthCalledWith(3, LogLevel.Error, 'ChildLogger: err msg four 4'); + }); +}); diff --git a/lib/logging/logger.ts b/lib/logging/logger.ts new file mode 100644 index 000000000..8414d544a --- /dev/null +++ b/lib/logging/logger.ts @@ -0,0 +1,167 @@ +/** + * Copyright 2019, 2024, 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OptimizelyError } from '../error/optimizly_error'; +import { MessageResolver } from '../message/message_resolver'; +import { sprintf } from '../utils/fns' + +export enum LogLevel { + Debug, + Info, + Warn, + Error, +} + +export const LogLevelToUpper: Record<LogLevel, string> = { + [LogLevel.Debug]: 'DEBUG', + [LogLevel.Info]: 'INFO', + [LogLevel.Warn]: 'WARN', + [LogLevel.Error]: 'ERROR', +}; + +export const LogLevelToLower: Record<LogLevel, string> = { + [LogLevel.Debug]: 'debug', + [LogLevel.Info]: 'info', + [LogLevel.Warn]: 'warn', + [LogLevel.Error]: 'error', +}; + +export interface LoggerFacade { + info(message: string | Error, ...args: any[]): void; + debug(message: string | Error, ...args: any[]): void; + warn(message: string | Error, ...args: any[]): void; + error(message: string | Error, ...args: any[]): void; + child(name?: string): LoggerFacade; + setName(name: string): void; +} + +export interface LogHandler { + log(level: LogLevel, message: string, ...args: any[]): void +} + +export class ConsoleLogHandler implements LogHandler { + private prefix: string + + constructor(prefix?: string) { + this.prefix = prefix || '[OPTIMIZELY]' + } + + log(level: LogLevel, message: string) : void { + const log = `${this.prefix} - ${LogLevelToUpper[level]} ${this.getTime()} ${message}` + this.consoleLog(level, log) + } + + private getTime(): string { + return new Date().toISOString() + } + + private consoleLog(logLevel: LogLevel, log: string) : void { + const methodName: string = LogLevelToLower[logLevel]; + + const method: any = console[methodName as keyof Console] || console.log; + method.call(console, log); + } +} + +type OptimizelyLoggerConfig = { + logHandler: LogHandler, + infoMsgResolver?: MessageResolver, + errorMsgResolver: MessageResolver, + level: LogLevel, + name?: string, +}; + +export class OptimizelyLogger implements LoggerFacade { + private name?: string; + private prefix = ''; + private logHandler: LogHandler; + private infoResolver?: MessageResolver; + private errorResolver: MessageResolver; + private level: LogLevel; + + constructor(config: OptimizelyLoggerConfig) { + this.logHandler = config.logHandler; + this.infoResolver = config.infoMsgResolver; + this.errorResolver = config.errorMsgResolver; + this.level = config.level; + if (config.name) { + this.setName(config.name); + } + } + + child(name?: string): OptimizelyLogger { + return new OptimizelyLogger({ + logHandler: this.logHandler, + infoMsgResolver: this.infoResolver, + errorMsgResolver: this.errorResolver, + level: this.level, + name, + }); + } + + setName(name: string): void { + this.name = name; + this.prefix = `${name}: `; + } + + info(message: string | Error, ...args: any[]): void { + this.log(LogLevel.Info, message, args) + } + + debug(message: string | Error, ...args: any[]): void { + this.log(LogLevel.Debug, message, args) + } + + warn(message: string | Error, ...args: any[]): void { + this.log(LogLevel.Warn, message, args) + } + + error(message: string | Error, ...args: any[]): void { + this.log(LogLevel.Error, message, args) + } + + private handleLog(level: LogLevel, message: string, args: any[]) { + const log = args.length > 0 ? `${this.prefix}${sprintf(message, ...args)}` + : `${this.prefix}${message}`; + + this.logHandler.log(level, log); + } + + private log(level: LogLevel, message: string | Error, args: any[]): void { + if (level < this.level) { + return; + } + + if (message instanceof Error) { + if (message instanceof OptimizelyError) { + message.setMessage(this.errorResolver); + } + this.handleLog(level, message.message, []); + return; + } + + let resolver = this.errorResolver; + + if (level < LogLevel.Warn) { + if (!this.infoResolver) { + return; + } + resolver = this.infoResolver; + } + + const resolvedMessage = resolver.resolve(message); + this.handleLog(level, resolvedMessage, args); + } +} diff --git a/lib/logging/logger_factory.spec.ts b/lib/logging/logger_factory.spec.ts new file mode 100644 index 000000000..bc7671008 --- /dev/null +++ b/lib/logging/logger_factory.spec.ts @@ -0,0 +1,105 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('./logger', async (importOriginal) => { + const actual = await importOriginal() + + const MockLogger = vi.fn(); + const MockConsoleLogHandler = vi.fn(); + + return { ...actual as any, OptimizelyLogger: MockLogger, ConsoleLogHandler: MockConsoleLogHandler }; +}); + +import { OptimizelyLogger, ConsoleLogHandler, LogLevel } from './logger'; +import { createLogger, extractLogger, INFO } from './logger_factory'; +import { errorResolver, infoResolver } from '../message/message_resolver'; + +describe('createLogger', () => { + const MockedOptimizelyLogger = vi.mocked(OptimizelyLogger); + const MockedConsoleLogHandler = vi.mocked(ConsoleLogHandler); + + beforeEach(() => { + MockedConsoleLogHandler.mockClear(); + MockedOptimizelyLogger.mockClear(); + }); + + it('should throw an error if the provided logHandler is not a valid LogHandler', () => { + expect(() => createLogger({ + level: INFO, + logHandler: {} as any, + })).toThrow('Invalid log handler'); + + expect(() => createLogger({ + level: INFO, + logHandler: { log: 'abc' } as any, + })).toThrow('Invalid log handler'); + + expect(() => createLogger({ + level: INFO, + logHandler: 'abc' as any, + })).toThrow('Invalid log handler'); + }); + + it('should throw an error if the level is not a valid level preset', () => { + expect(() => createLogger({ + level: null as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: undefined as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: 'abc' as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: 123 as any, + })).toThrow('Invalid level preset'); + + expect(() => createLogger({ + level: {} as any, + })).toThrow('Invalid level preset'); + }); + + it('should use the passed in options and a default name Optimizely', () => { + const mockLogHandler = { log: vi.fn() }; + + const logger = extractLogger(createLogger({ + level: INFO, + logHandler: mockLogHandler, + })); + + expect(logger).toBe(MockedOptimizelyLogger.mock.instances[0]); + const { name, level, infoMsgResolver, errorMsgResolver, logHandler } = MockedOptimizelyLogger.mock.calls[0][0]; + expect(name).toBe('Optimizely'); + expect(level).toBe(LogLevel.Info); + expect(infoMsgResolver).toBe(infoResolver); + expect(errorMsgResolver).toBe(errorResolver); + expect(logHandler).toBe(mockLogHandler); + }); + + it('should use a ConsoleLogHandler if no logHandler is provided', () => { + const logger = extractLogger(createLogger({ + level: INFO, + })); + + expect(logger).toBe(MockedOptimizelyLogger.mock.instances[0]); + const { logHandler } = MockedOptimizelyLogger.mock.calls[0][0]; + expect(logHandler).toBe(MockedConsoleLogHandler.mock.instances[0]); + }); +}); \ No newline at end of file diff --git a/lib/logging/logger_factory.ts b/lib/logging/logger_factory.ts new file mode 100644 index 000000000..2aee1b535 --- /dev/null +++ b/lib/logging/logger_factory.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConsoleLogHandler, LogHandler, LogLevel, OptimizelyLogger } from './logger'; +import { errorResolver, infoResolver, MessageResolver } from '../message/message_resolver'; +import { Maybe } from '../utils/type'; + +export const INVALID_LOG_HANDLER = 'Invalid log handler'; +export const INVALID_LEVEL_PRESET = 'Invalid level preset'; + +type LevelPreset = { + level: LogLevel, + infoResolver?: MessageResolver, + errorResolver: MessageResolver, +} + +const debugPreset: LevelPreset = { + level: LogLevel.Debug, + infoResolver, + errorResolver, +}; + +const infoPreset: LevelPreset = { + level: LogLevel.Info, + infoResolver, + errorResolver, +} + +const warnPreset: LevelPreset = { + level: LogLevel.Warn, + errorResolver, +} + +const errorPreset: LevelPreset = { + level: LogLevel.Error, + errorResolver, +} + +const levelPresetSymbol = Symbol(); + +export type OpaqueLevelPreset = { + [levelPresetSymbol]: unknown; +}; + +export const DEBUG: OpaqueLevelPreset = { + [levelPresetSymbol]: debugPreset, +}; + +export const INFO: OpaqueLevelPreset = { + [levelPresetSymbol]: infoPreset, +}; + +export const WARN: OpaqueLevelPreset = { + [levelPresetSymbol]: warnPreset, +}; + +export const ERROR: OpaqueLevelPreset = { + [levelPresetSymbol]: errorPreset, +}; + +export const extractLevelPreset = (preset: OpaqueLevelPreset): LevelPreset => { + if (!preset || typeof preset !== 'object' || !preset[levelPresetSymbol]) { + throw new Error(INVALID_LEVEL_PRESET); + } + return preset[levelPresetSymbol] as LevelPreset; +} + +const loggerSymbol = Symbol(); + +export type OpaqueLogger = { + [loggerSymbol]: unknown; +}; + +export type LoggerConfig = { + level: OpaqueLevelPreset, + logHandler?: LogHandler, +}; + +const validateLogHandler = (logHandler: any) => { + if (typeof logHandler !== 'object' || typeof logHandler.log !== 'function') { + throw new Error(INVALID_LOG_HANDLER); + } +} + +export const createLogger = (config: LoggerConfig): OpaqueLogger => { + const { level, infoResolver, errorResolver } = extractLevelPreset(config.level); + + if (config.logHandler) { + validateLogHandler(config.logHandler); + } + + const loggerName = 'Optimizely'; + + return { + [loggerSymbol]: new OptimizelyLogger({ + name: loggerName, + level, + infoMsgResolver: infoResolver, + errorMsgResolver: errorResolver, + logHandler: config.logHandler || new ConsoleLogHandler(), + }), + }; +}; + +export const wrapLogger = (logger: OptimizelyLogger): OpaqueLogger => { + return { + [loggerSymbol]: logger, + }; +}; + +export const extractLogger = (logger: Maybe<OpaqueLogger>): Maybe<OptimizelyLogger> => { + if (!logger || typeof logger !== 'object') { + return undefined; + } + + return logger[loggerSymbol] as Maybe<OptimizelyLogger>; +}; diff --git a/lib/message/error_message.ts b/lib/message/error_message.ts new file mode 100644 index 000000000..720baa377 --- /dev/null +++ b/lib/message/error_message.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const NOTIFICATION_LISTENER_EXCEPTION = 'Notification listener for (%s) threw exception: %s'; +export const CONDITION_EVALUATOR_ERROR = 'Error evaluating audience condition of type %s: %s'; +export const EXPERIMENT_KEY_NOT_IN_DATAFILE = 'Experiment key %s is not in datafile.'; +export const FEATURE_NOT_IN_DATAFILE = 'Feature key %s is not in datafile.'; +export const INVALID_ATTRIBUTES = 'Provided attributes are in an invalid format.'; +export const INVALID_BUCKETING_ID = 'Unable to generate hash for bucketing ID %s: %s'; +export const INVALID_DATAFILE = 'Datafile is invalid - property %s: %s'; +export const INVALID_DATAFILE_MALFORMED = 'Datafile is invalid because it is malformed.'; +export const INVALID_CONFIG = 'Provided Optimizely config is in an invalid format.'; +export const INVALID_JSON = 'JSON object is not valid.'; +export const INVALID_EVENT_TAGS = 'Provided event tags are in an invalid format.'; +export const INVALID_EXPERIMENT_KEY = + 'Experiment key %s is not in datafile. It is either invalid, paused, or archived.'; +export const INVALID_EXPERIMENT_ID = 'Experiment ID %s is not in datafile.'; +export const INVALID_GROUP_ID = 'Group ID %s is not in datafile.'; +export const INVALID_USER_ID = 'Provided user ID is in an invalid format.'; +export const INVALID_USER_PROFILE_SERVICE = 'Provided user profile service instance is in an invalid format: %s.'; +export const MISSING_INTEGRATION_KEY = + 'Integration key missing from datafile. All integrations should include a key.'; +export const NO_DATAFILE_SPECIFIED = 'No datafile specified. Cannot start optimizely.'; +export const NO_JSON_PROVIDED = 'No JSON object to validate against schema.'; +export const NO_EVENT_PROCESSOR = 'No event processor is provided'; +export const NO_VARIATION_FOR_EXPERIMENT_KEY = 'No variation key %s defined in datafile for experiment %s.'; +export const ODP_CONFIG_NOT_AVAILABLE = 'ODP config is not available.'; +export const ODP_EVENT_FAILED = 'ODP event send failed.'; +export const ODP_EVENTS_SHOULD_HAVE_ATLEAST_ONE_KEY_VALUE = 'ODP events should have at least one key-value pair in identifiers.'; +export const ODP_EVENT_FAILED_ODP_MANAGER_MISSING = 'ODP Event failed to send. (ODP Manager not available).'; +export const ODP_NOT_INTEGRATED = 'ODP is not integrated'; +export const UNDEFINED_ATTRIBUTE = 'Provided attribute: %s has an undefined value.'; +export const UNRECOGNIZED_ATTRIBUTE = + 'Unrecognized attribute %s provided. Pruning before sending event to Optimizely.'; +export const UNABLE_TO_CAST_VALUE = 'Unable to cast value %s to type %s, returning null.'; +export const USER_NOT_IN_FORCED_VARIATION = + 'User %s is not in the forced variation map. Cannot remove their forced variation.'; +export const USER_PROFILE_LOOKUP_ERROR = 'Error while looking up user profile for user ID "%s": %s.'; +export const USER_PROFILE_SAVE_ERROR = 'Error while saving user profile for user ID "%s": %s.'; +export const VARIABLE_KEY_NOT_IN_DATAFILE = + 'Variable with key "%s" associated with feature with key "%s" is not in datafile.'; +export const VARIATION_ID_NOT_IN_DATAFILE = 'Variation ID %s is not in the datafile.'; +export const INVALID_INPUT_FORMAT = 'Provided %s is in an invalid format.'; +export const INVALID_DATAFILE_VERSION = + 'This version of the JavaScript SDK does not support the given datafile version: %s'; +export const INVALID_VARIATION_KEY = 'Provided variation key is in an invalid format.'; +export const ERROR_FETCHING_DATAFILE = 'Error fetching datafile: %s'; +export const DATAFILE_FETCH_REQUEST_FAILED = 'Datafile fetch request failed with status: %s'; +export const EVENT_DATA_INVALID = 'Event data invalid.'; +export const EVENT_ACTION_INVALID = 'Event action invalid.'; +export const FAILED_TO_SEND_ODP_EVENTS = 'failed to send odp events'; +export const UNABLE_TO_GET_VUID_VUID_MANAGER_NOT_AVAILABLE = 'Unable to get VUID - VuidManager is not available' +export const UNKNOWN_CONDITION_TYPE = + 'Audience condition %s has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.'; +export const UNKNOWN_MATCH_TYPE = + 'Audience condition %s uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK.'; +export const UNRECOGNIZED_DECIDE_OPTION = 'Unrecognized decide option %s provided.'; +export const NO_PROJECT_CONFIG_FAILURE = 'No project config available. Failing %s.'; +export const EVENT_KEY_NOT_FOUND = 'Event key %s is not in datafile.'; +export const NOT_TRACKING_USER = 'Not tracking user %s.'; +export const VARIABLE_REQUESTED_WITH_WRONG_TYPE = + 'Requested variable type "%s", but variable is of type "%s". Use correct API to retrieve value. Returning None.'; +export const UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX = + 'Attribute %s unexpectedly has reserved prefix %s; using attribute ID instead of reserved attribute name.'; +export const BUCKETING_ID_NOT_STRING = 'BucketingID attribute is not a string. Defaulted to userId'; +export const UNEXPECTED_CONDITION_VALUE = + 'Audience condition %s evaluated to UNKNOWN because the condition value is not supported.'; +export const UNEXPECTED_TYPE = + 'Audience condition %s evaluated to UNKNOWN because a value of type "%s" was passed for user attribute "%s".'; +export const OUT_OF_BOUNDS = + 'Audience condition %s evaluated to UNKNOWN because the number value for user attribute "%s" is not in the range [-2^53, +2^53].'; +export const REQUEST_TIMEOUT = 'Request timeout'; +export const REQUEST_ERROR = 'Request error'; +export const NO_STATUS_CODE_IN_RESPONSE = 'No status code in response'; +export const UNSUPPORTED_PROTOCOL = 'Unsupported protocol: %s'; +export const RETRY_CANCELLED = 'Retry cancelled'; +export const ONLY_POST_REQUESTS_ARE_SUPPORTED = 'Only POST requests are supported'; +export const SEND_BEACON_FAILED = 'sendBeacon failed'; +export const FAILED_TO_DISPATCH_EVENTS = 'Failed to dispatch events, status: %s'; +export const ODP_EVENT_MANAGER_STOPPED = "ODP event manager stopped before it could start"; +export const UNABLE_TO_ATTACH_UNLOAD = 'unable to bind optimizely.close() to page unload event: "%s"'; +export const UNABLE_TO_PARSE_AND_SKIPPED_HEADER = 'Unable to parse & skipped header item'; +export const CMAB_FETCH_FAILED = 'CMAB decision fetch failed with status: %s'; +export const INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'; +export const PROMISE_NOT_ALLOWED = "Promise value is not allowed in sync operation"; +export const SERVICE_NOT_RUNNING = "%s not running"; + +export const messages: string[] = []; diff --git a/lib/message/log_message.ts b/lib/message/log_message.ts new file mode 100644 index 000000000..c27f5076f --- /dev/null +++ b/lib/message/log_message.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const FEATURE_ENABLED_FOR_USER = 'Feature %s is enabled for user %s.'; +export const FEATURE_NOT_ENABLED_FOR_USER = 'Feature %s is not enabled for user %s.'; +export const FAILED_TO_PARSE_VALUE = 'Failed to parse event value "%s" from event tags.'; +export const FAILED_TO_PARSE_REVENUE = 'Failed to parse revenue value "%s" from event tags.'; +export const INVALID_CLIENT_ENGINE = 'Invalid client engine passed: %s. Defaulting to node-sdk.'; +export const INVALID_DEFAULT_DECIDE_OPTIONS = 'Provided default decide options is not an array.'; +export const INVALID_DECIDE_OPTIONS = 'Provided decide options is not an array. Using default decide options.'; +export const NOT_ACTIVATING_USER = 'Not activating user %s for experiment %s.'; +export const PARSED_REVENUE_VALUE = 'Parsed revenue value "%s" from event tags.'; +export const PARSED_NUMERIC_VALUE = 'Parsed event value "%s" from event tags.'; +export const SAVED_USER_VARIATION = 'Saved user profile for user "%s".'; +export const SAVED_VARIATION_NOT_FOUND = + 'User %s was previously bucketed into variation with ID %s for experiment %s, but no matching variation was found.'; +export const SHOULD_NOT_DISPATCH_ACTIVATE = 'Experiment %s is not in "Running" state. Not activating user.'; +export const SKIPPING_JSON_VALIDATION = 'Skipping JSON schema validation.'; +export const TRACK_EVENT = 'Tracking event %s for user %s.'; +export const USER_MAPPED_TO_FORCED_VARIATION = + 'Set variation %s for experiment %s and user %s in the forced variation map.'; +export const USER_HAS_NO_FORCED_VARIATION = 'User %s is not in the forced variation map.'; +export const USER_RECEIVED_DEFAULT_VARIABLE_VALUE = + 'User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".'; +export const FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE = + 'Feature "%s" is not enabled for user %s. Returning the default variable value "%s".'; +export const VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE = + 'Variable "%s" is not used in variation "%s". Returning default value.'; +export const USER_RECEIVED_VARIABLE_VALUE = 'Got variable value "%s" for variable "%s" of feature flag "%s"'; +export const VALID_DATAFILE = 'Datafile is valid.'; +export const VALID_USER_PROFILE_SERVICE = 'Valid user profile service provided.'; +export const VARIATION_REMOVED_FOR_USER = 'Variation mapped to experiment %s has been removed for user %s.'; + +export const VALID_BUCKETING_ID = 'BucketingId is valid: "%s"'; +export const EVALUATING_AUDIENCE = 'Starting to evaluate audience "%s" with conditions: %s.'; +export const AUDIENCE_EVALUATION_RESULT = 'Audience "%s" evaluated to %s.'; +export const MISSING_ATTRIBUTE_VALUE = + 'Audience condition %s evaluated to UNKNOWN because no value was passed for user attribute "%s".'; +export const UNEXPECTED_TYPE_NULL = + 'Audience condition %s evaluated to UNKNOWN because a null value was passed for user attribute "%s".'; +export const UPDATED_OPTIMIZELY_CONFIG = 'Updated Optimizely config to revision %s (project id %s)'; +export const ADDING_AUTHORIZATION_HEADER_WITH_BEARER_TOKEN = 'Adding Authorization header with Bearer Token'; +export const MAKING_DATAFILE_REQ_TO_URL_WITH_HEADERS = 'Making datafile request to url %s with headers: %s'; +export const RESPONSE_STATUS_CODE = 'Response status code: %s'; +export const SAVED_LAST_MODIFIED_HEADER_VALUE_FROM_RESPONSE = 'Saved last modified header value from response: %s'; +export const USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT = + 'No experiment %s mapped to user %s in the forced variation map.'; +export const INVALID_EXPERIMENT_KEY_INFO = + 'Experiment key %s is not in datafile. It is either invalid, paused, or archived.'; +export const EVENT_STORE_FULL = 'Event store is full. Not saving event with id %d.'; + +export const messages: string[] = []; diff --git a/lib/message/message_resolver.ts b/lib/message/message_resolver.ts new file mode 100644 index 000000000..07a0cefdf --- /dev/null +++ b/lib/message/message_resolver.ts @@ -0,0 +1,20 @@ +import { messages as infoMessages } from 'log_message'; +import { messages as errorMessages } from 'error_message'; + +export interface MessageResolver { + resolve(baseMessage: string): string; +} + +export const infoResolver: MessageResolver = { + resolve(baseMessage: string): string { + const messageNum = parseInt(baseMessage); + return infoMessages[messageNum] || baseMessage; + } +}; + +export const errorResolver: MessageResolver = { + resolve(baseMessage: string): string { + const messageNum = parseInt(baseMessage); + return errorMessages[messageNum] || baseMessage; + } +}; diff --git a/lib/notification_center/index.spec.ts b/lib/notification_center/index.spec.ts new file mode 100644 index 000000000..4ba54a0c3 --- /dev/null +++ b/lib/notification_center/index.spec.ts @@ -0,0 +1,606 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, beforeEach, it, vi, expect } from 'vitest'; +import { createNotificationCenter, DefaultNotificationCenter } from './'; +import { + ActivateListenerPayload, + DecisionListenerPayload, + LogEventListenerPayload, + NOTIFICATION_TYPES, + TrackListenerPayload, + OptimizelyConfigUpdateListenerPayload, +} from './type'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { LoggerFacade } from '../logging/logger'; + +describe('addNotificationListener', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should return -1 if notification type is not a valid type', () => { + const INVALID_LISTENER_TYPE = 'INVALID_LISTENER_TYPE' as const; + const mockFn = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const listenerId = notificationCenterInstance.addNotificationListener(INVALID_LISTENER_TYPE, mockFn); + + expect(listenerId).toBe(-1); + }); + + it('should return an id (listernId) > 0 of the notification listener if callback is not already added', () => { + const activateCallback = vi.fn(); + const decisionCallback = vi.fn(); + const logEventCallback = vi.fn(); + const configUpdateCallback = vi.fn(); + const trackCallback = vi.fn(); + // store a listenerId for each type + const activateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallback + ); + const decisionListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallback + ); + const logEventListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallback + ); + const configUpdateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallback + ); + const trackListenerId = notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallback); + + expect(activateListenerId).toBeGreaterThan(0); + expect(decisionListenerId).toBeGreaterThan(0); + expect(logEventListenerId).toBeGreaterThan(0); + expect(configUpdateListenerId).toBeGreaterThan(0); + expect(trackListenerId).toBeGreaterThan(0); + }); +}); + +describe('removeNotificationListener', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should return false if listernId does not exist', () => { + const notListenerId = notificationCenterInstance.removeNotificationListener(5); + + expect(notListenerId).toBe(false); + }); + + it('should return true when eixsting listener is removed', () => { + const activateCallback = vi.fn(); + const decisionCallback = vi.fn(); + const logEventCallback = vi.fn(); + const configUpdateCallback = vi.fn(); + const trackCallback = vi.fn(); + // add listeners for each type + const activateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallback + ); + const decisionListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallback + ); + const logEventListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallback + ); + const configListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallback + ); + const trackListenerId = notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallback); + // remove listeners for each type + const activateListenerRemoved = notificationCenterInstance.removeNotificationListener(activateListenerId); + const decisionListenerRemoved = notificationCenterInstance.removeNotificationListener(decisionListenerId); + const logEventListenerRemoved = notificationCenterInstance.removeNotificationListener(logEventListenerId); + const trackListenerRemoved = notificationCenterInstance.removeNotificationListener(trackListenerId); + const configListenerRemoved = notificationCenterInstance.removeNotificationListener(configListenerId); + + expect(activateListenerRemoved).toBe(true); + expect(decisionListenerRemoved).toBe(true); + expect(logEventListenerRemoved).toBe(true); + expect(trackListenerRemoved).toBe(true); + expect(configListenerRemoved).toBe(true); + }); + it('should only remove the specified listener', () => { + const activateCallbackSpy1 = vi.fn(); + const activateCallbackSpy2 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const decisionCallbackSpy2 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const logEventCallbackSpy2 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy2 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + const trackCallbackSpy2 = vi.fn(); + // register listeners for each type + const activateListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallbackSpy1 + ); + const decisionListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallbackSpy1 + ); + const logeventlistenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallbackSpy1 + ); + const configUpdateListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + const trackListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackCallbackSpy1 + ); + // register second listeners for each type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // remove first listener + const activateListenerRemoved1 = notificationCenterInstance.removeNotificationListener(activateListenerId1); + const decisionListenerRemoved1 = notificationCenterInstance.removeNotificationListener(decisionListenerId1); + const logEventListenerRemoved1 = notificationCenterInstance.removeNotificationListener(logeventlistenerId1); + const configUpdateListenerRemoved1 = notificationCenterInstance.removeNotificationListener(configUpdateListenerId1); + const trackListenerRemoved1 = notificationCenterInstance.removeNotificationListener(trackListenerId1); + // send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(activateListenerRemoved1).toBe(true); + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(activateCallbackSpy2).toHaveBeenCalledTimes(1); + expect(decisionListenerRemoved1).toBe(true); + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy2).toHaveBeenCalledTimes(1); + expect(logEventListenerRemoved1).toBe(true); + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy2).toHaveBeenCalledTimes(1); + expect(configUpdateListenerRemoved1).toBe(true); + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy2).toHaveBeenCalledTimes(1); + expect(trackListenerRemoved1).toBe(true); + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy2).toHaveBeenCalledTimes(1); + }); +}); + +describe('clearAllNotificationListeners', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should remove all notification listeners for all types', () => { + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove all listeners + notificationCenterInstance.clearAllNotificationListeners(); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + }); +}); + +describe('clearNotificationListeners', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + + it('should remove all notification listeners for the ACTIVATE type', () => { + const activateCallbackSpy1 = vi.fn(); + const activateCallbackSpy2 = vi.fn(); + //add 2 different listeners for ACTIVATE + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + // remove ACTIVATE listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(activateCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the DECISION type', () => { + const decisionCallbackSpy1 = vi.fn(); + const decisionCallbackSpy2 = vi.fn(); + //add 2 different listeners for DECISION + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + // remove DECISION listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.DECISION); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the LOG_EVENT type', () => { + const logEventCallbackSpy1 = vi.fn(); + const logEventCallbackSpy2 = vi.fn(); + //add 2 different listeners for LOG_EVENT + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + // remove LOG_EVENT listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the OPTIMIZELY_CONFIG_UPDATE type', () => { + const configUpdateCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy2 = vi.fn(); + //add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + // remove OPTIMIZELY_CONFIG_UPDATE listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + // trigger send notifications + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should remove all notification listeners for the TRACK type', () => { + const trackCallbackSpy1 = vi.fn(); + const trackCallbackSpy2 = vi.fn(); + //add 2 different listeners for TRACK + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // remove TRACK listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.TRACK); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy2).not.toHaveBeenCalled(); + }); + + it('should only remove ACTIVATE type listeners and not any other types', () => { + const activateCallbackSpy1 = vi.fn(); + const activateCallbackSpy2 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + //add 2 different listeners for ACTIVATE + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only ACTIVATE type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(activateCallbackSpy1).not.toHaveBeenCalled(); + expect(activateCallbackSpy2).not.toHaveBeenCalled(); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove DECISION type listeners and not any other types', () => { + const decisionCallbackSpy1 = vi.fn(); + const decisionCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add 2 different listeners for DECISION + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only DECISION type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.DECISION); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(decisionCallbackSpy1).not.toHaveBeenCalled(); + expect(decisionCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove LOG_EVENT type listeners and not any other types', () => { + const logEventCallbackSpy1 = vi.fn(); + const logEventCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add 2 different listeners for LOG_EVENT + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only LOG_EVENT type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(logEventCallbackSpy1).not.toHaveBeenCalled(); + expect(logEventCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove OPTIMIZELY_CONFIG_UPDATE type listeners and not any other types', () => { + const configUpdateCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only OPTIMIZELY_CONFIG_UPDATE type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(configUpdateCallbackSpy1).not.toHaveBeenCalled(); + expect(configUpdateCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(trackCallbackSpy1).toHaveBeenCalledTimes(1); + }); + + it('should only remove TRACK type listeners and not any other types', () => { + const trackCallbackSpy1 = vi.fn(); + const trackCallbackSpy2 = vi.fn(); + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + // add 2 different listeners for TRACK + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + // remove only TRACK type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.TRACK); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {} as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {} as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {} as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + ({} as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {} as TrackListenerPayload); + + expect(trackCallbackSpy1).not.toHaveBeenCalled(); + expect(trackCallbackSpy2).not.toHaveBeenCalled(); + expect(activateCallbackSpy1).toHaveBeenCalledTimes(1); + expect(decisionCallbackSpy1).toHaveBeenCalledTimes(1); + expect(logEventCallbackSpy1).toHaveBeenCalledTimes(1); + expect(configUpdateCallbackSpy1).toHaveBeenCalledTimes(1); + }); +}); + +describe('sendNotifications', () => { + let logger: LoggerFacade; + let notificationCenterInstance: DefaultNotificationCenter; + + beforeEach(() => { + logger = getMockLogger(); + notificationCenterInstance = createNotificationCenter({ logger }); + }); + it('should call the listener callback with exact arguments', () => { + const activateCallbackSpy1 = vi.fn(); + const decisionCallbackSpy1 = vi.fn(); + const logEventCallbackSpy1 = vi.fn(); + const configUpdateCallbackSpy1 = vi.fn(); + const trackCallbackSpy1 = vi.fn(); + // listener object data for each type + const activateData = { + experiment: {}, + userId: '', + attributes: {}, + variation: {}, + logEvent: {}, + }; + const decisionData = { + type: '', + userId: 'use1', + attributes: {}, + decisionInfo: {}, + }; + const logEventData = { + url: '', + httpVerb: '', + params: {}, + }; + const configUpdateData = {}; + const trackData = { + eventKey: '', + userId: '', + attributes: {}, + eventTags: {}, + }; + // add listeners + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, activateData as ActivateListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, decisionData as DecisionListenerPayload); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, logEventData as LogEventListenerPayload); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + (configUpdateData as unknown) as OptimizelyConfigUpdateListenerPayload + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, trackData as TrackListenerPayload); + + expect(activateCallbackSpy1).toHaveBeenCalledWith(activateData); + expect(decisionCallbackSpy1).toHaveBeenCalledWith(decisionData); + expect(logEventCallbackSpy1).toHaveBeenCalledWith(logEventData); + expect(configUpdateCallbackSpy1).toHaveBeenCalledWith(configUpdateData); + expect(trackCallbackSpy1).toHaveBeenCalledWith(trackData); + }); +}); diff --git a/lib/notification_center/index.tests.js b/lib/notification_center/index.tests.js new file mode 100644 index 000000000..11e6da2bb --- /dev/null +++ b/lib/notification_center/index.tests.js @@ -0,0 +1,597 @@ +/** + * Copyright 2020, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sinon from 'sinon'; +import { assert } from 'chai'; + +import { createNotificationCenter } from './'; +import * as enums from '../utils/enums'; +import { NOTIFICATION_TYPES } from './type'; + +var LOG_LEVEL = enums.LOG_LEVEL; + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +describe('lib/core/notification_center', function() { + describe('APIs', function() { + var mockLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + var mockLoggerStub; + + var notificationCenterInstance; + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + mockLoggerStub = sandbox.stub(mockLogger, 'log'); + + notificationCenterInstance = createNotificationCenter({ + logger: mockLoggerStub, + }); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#addNotificationListener', function() { + context('the listener type is not a valid type', function() { + it('should return -1 if notification type is not a valid type', function() { + var INVALID_LISTENER_TYPE = 'INVALID_LISTENER_TYPE'; + var genericCallbackSpy = sinon.spy(); + + var listenerId = notificationCenterInstance.addNotificationListener( + INVALID_LISTENER_TYPE, + genericCallbackSpy + ); + assert.strictEqual(listenerId, -1); + }); + }); + + context('the listener type is a valid type', function() { + it('should return an id (listenerId) > 0 of the notification listener if callback is not already added', function() { + var activateCallback; + var decisionCallback; + var logEventCallback; + var configUpdateCallback; + var trackCallback; + // store a listenerId for each type + var activateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallback + ); + var decisionListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallback + ); + var logEventListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallback + ); + var configUpdateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallback + ); + var trackListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackCallback + ); + // assertions + assert.isAbove(activateListenerId, 0); + assert.isAbove(decisionListenerId, 0); + assert.isAbove(logEventListenerId, 0); + assert.isAbove(configUpdateListenerId, 0); + assert.isAbove(trackListenerId, 0); + }); + }); + }); + + describe('#removeNotificationListener', function() { + context('the listenerId does not exist', function() { + it('should return false if listenerId does not exist', function() { + var notListenerId = notificationCenterInstance.removeNotificationListener(5); + assert.isFalse(notListenerId); + }); + }); + + context('listenerId exists', function() { + it('should return true when existing listener is removed', function() { + var activateCallback; + var decisionCallback; + var logEventCallback; + var configUpdateCallback; + var trackCallback; + // add listeners for each type + var activateListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallback + ); + var decisionListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallback + ); + var logEventListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallback + ); + var configListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallback + ); + var trackListenerId = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackCallback + ); + // remove listeners for each type + var activateListenerRemoved = notificationCenterInstance.removeNotificationListener(activateListenerId); + var decisionListenerRemoved = notificationCenterInstance.removeNotificationListener(decisionListenerId); + var logEventListenerRemoved = notificationCenterInstance.removeNotificationListener(logEventListenerId); + var trackListenerRemoved = notificationCenterInstance.removeNotificationListener(trackListenerId); + var configListenerRemoved = notificationCenterInstance.removeNotificationListener(configListenerId); + + // assertions + assert.strictEqual(activateListenerRemoved, true); + assert.strictEqual(decisionListenerRemoved, true); + assert.strictEqual(logEventListenerRemoved, true); + assert.strictEqual(trackListenerRemoved, true); + assert.strictEqual(configListenerRemoved, true); + }); + + it('should only remove the specified listener', function() { + var activateCallbackSpy1 = sinon.spy(); + var activateCallbackSpy2 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy2 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy2 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy2 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + var trackCallbackSpy2 = sinon.spy(); + // register listeners for each type + var activateListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateCallbackSpy1 + ); + var decisionListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionCallbackSpy1 + ); + var logeventlistenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + logEventCallbackSpy1 + ); + var configUpdateListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + var trackListenerId1 = notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackCallbackSpy1 + ); + // register second listeners for each type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // remove first listener + var activateListenerRemoved1 = notificationCenterInstance.removeNotificationListener(activateListenerId1); + var decisionListenerRemoved1 = notificationCenterInstance.removeNotificationListener(decisionListenerId1); + var logEventListenerRemoved1 = notificationCenterInstance.removeNotificationListener(logeventlistenerId1); + var configUpdateListenerRemoved1 = notificationCenterInstance.removeNotificationListener( + configUpdateListenerId1 + ); + var trackListenerRemoved1 = notificationCenterInstance.removeNotificationListener(trackListenerId1); + // send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // Assertions + assert.strictEqual(activateListenerRemoved1, true); + sinon.assert.notCalled(activateCallbackSpy1); + sinon.assert.calledOnce(activateCallbackSpy2); + assert.strictEqual(decisionListenerRemoved1, true); + sinon.assert.notCalled(decisionCallbackSpy1); + sinon.assert.calledOnce(decisionCallbackSpy2); + assert.strictEqual(logEventListenerRemoved1, true); + sinon.assert.notCalled(logEventCallbackSpy1); + sinon.assert.calledOnce(logEventCallbackSpy2); + assert.strictEqual(configUpdateListenerRemoved1, true); + sinon.assert.notCalled(configUpdateCallbackSpy1); + sinon.assert.calledOnce(configUpdateCallbackSpy2); + assert.strictEqual(trackListenerRemoved1, true); + sinon.assert.notCalled(trackCallbackSpy1); + sinon.assert.calledOnce(trackCallbackSpy2); + }); + }); + }); + + describe('#clearAllNotificationListeners', function() { + it('should remove all notification listeners for all types', function() { + var activateCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove all listeners + notificationCenterInstance.clearAllNotificationListeners(); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that none of the now removed listeners were called + sinon.assert.notCalled(activateCallbackSpy1); + sinon.assert.notCalled(decisionCallbackSpy1); + sinon.assert.notCalled(logEventCallbackSpy1); + sinon.assert.notCalled(configUpdateCallbackSpy1); + sinon.assert.notCalled(trackCallbackSpy1); + }); + }); + + describe('#clearNotificationListeners', function() { + context('there is only one type of listener added', function() { + it('should remove all notification listeners for the ACTIVATE type', function() { + var activateCallbackSpy1 = sinon.spy(); + var activateCallbackSpy2 = sinon.spy(); + //add 2 different listeners for ACTIVATE + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + // remove ACTIVATE listeners + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + // check that none of the ACTIVATE listeners were called + sinon.assert.notCalled(activateCallbackSpy1); + sinon.assert.notCalled(activateCallbackSpy2); + }); + + it('should remove all notification listeners for the DECISION type', function() { + var decisionCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy2 = sinon.spy(); + //add 2 different listeners for DECISION + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + // remove DECISION listeners + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.DECISION); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + // check that none of the DECISION listeners were called + sinon.assert.notCalled(decisionCallbackSpy1); + sinon.assert.notCalled(decisionCallbackSpy2); + }); + + it('should remove all notification listeners for the LOG_EVENT type', function() { + var logEventCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy2 = sinon.spy(); + //add 2 different listeners for LOG_EVENT + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + // remove LOG_EVENT listeners + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + // check that none of the LOG_EVENT listeners were called + sinon.assert.notCalled(logEventCallbackSpy1); + sinon.assert.notCalled(logEventCallbackSpy2); + }); + + it('should remove all notification listeners for the OPTIMIZELY_CONFIG_UPDATE type', function() { + var configUpdateCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy2 = sinon.spy(); + //add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + // remove OPTIMIZELY_CONFIG_UPDATE listeners + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + // check that none of the OPTIMIZELY_CONFIG_UPDATE listeners were called + sinon.assert.notCalled(configUpdateCallbackSpy1); + sinon.assert.notCalled(configUpdateCallbackSpy2); + }); + + it('should remove all notification listeners for the TRACK type', function() { + var trackCallbackSpy1 = sinon.spy(); + var trackCallbackSpy2 = sinon.spy(); + //add 2 different listeners for TRACK + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // remove TRACK listeners + notificationCenterInstance.clearAllNotificationListeners(NOTIFICATION_TYPES.TRACK); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that none of the TRACK listeners were called + sinon.assert.notCalled(trackCallbackSpy1); + sinon.assert.notCalled(trackCallbackSpy2); + }); + }); + + context('there is more than one type of listener added', function() { + it('should only remove ACTIVATE type listeners and not any other types', function() { + var activateCallbackSpy1 = sinon.spy(); + var activateCallbackSpy2 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + //add 2 different listeners for ACTIVATE + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only ACTIVATE type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that ACTIVATE listeners were note called + sinon.assert.notCalled(activateCallbackSpy1); + sinon.assert.notCalled(activateCallbackSpy2); + // check that all other listeners were called. + sinon.assert.calledOnce(decisionCallbackSpy1); + sinon.assert.calledOnce(logEventCallbackSpy1); + sinon.assert.calledOnce(configUpdateCallbackSpy1); + sinon.assert.calledOnce(trackCallbackSpy1); + }); + + it('should only remove DECISION type listeners and not any other types', function() { + var decisionCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy2 = sinon.spy(); + var activateCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + // add 2 different listeners for DECISION + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only DECISION type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.DECISION); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that DECISION listeners were not called + sinon.assert.notCalled(decisionCallbackSpy1); + sinon.assert.notCalled(decisionCallbackSpy2); + // check that all other listeners were called. + sinon.assert.calledOnce(activateCallbackSpy1); + sinon.assert.calledOnce(logEventCallbackSpy1); + sinon.assert.calledOnce(configUpdateCallbackSpy1); + sinon.assert.calledOnce(trackCallbackSpy1); + }); + + it('should only remove LOG_EVENT type listeners and not any other types', function() { + var logEventCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy2 = sinon.spy(); + var activateCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + // add 2 different listeners for LOG_EVENT + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only LOG_EVENT type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.LOG_EVENT); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that LOG_EVENT listeners were not called + sinon.assert.notCalled(logEventCallbackSpy1); + sinon.assert.notCalled(logEventCallbackSpy2); + // check that all other listeners were called. + sinon.assert.calledOnce(activateCallbackSpy1); + sinon.assert.calledOnce(decisionCallbackSpy1); + sinon.assert.calledOnce(configUpdateCallbackSpy1); + sinon.assert.calledOnce(trackCallbackSpy1); + }); + + it('should only remove OPTIMIZELY_CONFIG_UPDATE type listeners and not any other types', function() { + var configUpdateCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy2 = sinon.spy(); + var activateCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + // add 2 different listeners for OPTIMIZELY_CONFIG_UPDATE + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy2 + ); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // remove only OPTIMIZELY_CONFIG_UPDATE type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that OPTIMIZELY_CONFIG_UPDATE listeners were not called + sinon.assert.notCalled(configUpdateCallbackSpy1); + sinon.assert.notCalled(configUpdateCallbackSpy2); + // check that all other listeners were called. + sinon.assert.calledOnce(activateCallbackSpy1); + sinon.assert.calledOnce(decisionCallbackSpy1); + sinon.assert.calledOnce(logEventCallbackSpy1); + sinon.assert.calledOnce(trackCallbackSpy1); + }); + + it('should only remove TRACK type listeners and not any other types', function() { + var trackCallbackSpy1 = sinon.spy(); + var trackCallbackSpy2 = sinon.spy(); + var activateCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + // add 2 different listeners for TRACK + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy2); + // add a listener for each notification type + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + // remove only TRACK type + notificationCenterInstance.clearNotificationListeners(NOTIFICATION_TYPES.TRACK); + // trigger send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, {}); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, {}); + // check that TRACK listeners were not called + sinon.assert.notCalled(trackCallbackSpy1); + sinon.assert.notCalled(trackCallbackSpy2); + // check that all other listeners were called. + sinon.assert.calledOnce(activateCallbackSpy1); + sinon.assert.calledOnce(decisionCallbackSpy1); + sinon.assert.calledOnce(logEventCallbackSpy1); + sinon.assert.calledOnce(configUpdateCallbackSpy1); + }); + }); + }); + + describe('#sendNotifications', function() { + context('send notification for each type ', function() { + it('should call the listener callback with exact arguments', function() { + var activateCallbackSpy1 = sinon.spy(); + var decisionCallbackSpy1 = sinon.spy(); + var logEventCallbackSpy1 = sinon.spy(); + var configUpdateCallbackSpy1 = sinon.spy(); + var trackCallbackSpy1 = sinon.spy(); + // listener object data for each type + var activateData = { + experiment: {}, + userId: '', + attributes: {}, + variation: {}, + logEvent: {}, + }; + var decisionData = { + type: '', + userId: 'use1', + attributes: {}, + decisionInfo: {}, + }; + var logEventData = { + url: '', + httpVerb: '', + params: {}, + }; + var configUpdateData = {}; + var trackData = { + eventKey: '', + userId: '', + attributes: {}, + eventTags: {}, + }; + // add listeners + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionCallbackSpy1); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.LOG_EVENT, logEventCallbackSpy1); + notificationCenterInstance.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateCallbackSpy1 + ); + notificationCenterInstance.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackCallbackSpy1); + // send notifications + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, activateData); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.DECISION, decisionData); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, logEventData); + notificationCenterInstance.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + configUpdateData + ); + notificationCenterInstance.sendNotifications(NOTIFICATION_TYPES.TRACK, trackData); + // assertions + sinon.assert.calledWithExactly(activateCallbackSpy1, activateData); + sinon.assert.calledWithExactly(decisionCallbackSpy1, decisionData); + sinon.assert.calledWithExactly(logEventCallbackSpy1, logEventData); + sinon.assert.calledWithExactly(configUpdateCallbackSpy1, configUpdateData); + sinon.assert.calledWithExactly(trackCallbackSpy1, trackData); + }); + }); + }); + }); +}); diff --git a/lib/notification_center/index.ts b/lib/notification_center/index.ts new file mode 100644 index 000000000..7b17ba658 --- /dev/null +++ b/lib/notification_center/index.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2020, 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from '../logging/logger'; +import { objectValues } from '../utils/fns'; + +import { NOTIFICATION_TYPES } from './type'; +import { NotificationType, NotificationPayload } from './type'; +import { Consumer, Fn } from '../utils/type'; +import { EventEmitter } from '../utils/event_emitter/event_emitter'; +import { NOTIFICATION_LISTENER_EXCEPTION } from 'error_message'; +import { ErrorReporter } from '../error/error_reporter'; +import { ErrorNotifier } from '../error/error_notifier'; + +interface NotificationCenterOptions { + logger?: LoggerFacade; + errorNotifier?: ErrorNotifier; +} + +export interface NotificationCenter { + addNotificationListener<N extends NotificationType>( + notificationType: N, + callback: Consumer<NotificationPayload[N]> + ): number + removeNotificationListener(listenerId: number): boolean; + clearAllNotificationListeners(): void; + clearNotificationListeners(notificationType: NotificationType): void; +} + +export interface NotificationSender { + sendNotifications<N extends NotificationType>( + notificationType: N, + notificationData: NotificationPayload[N] + ): void; +} + +/** + * NotificationCenter allows registration and triggering of callback functions using + * notification event types defined in NOTIFICATION_TYPES of utils/enums/index.js: + * - ACTIVATE: An impression event will be sent to Optimizely. + * - TRACK a conversion event will be sent to Optimizely + */ +export class DefaultNotificationCenter implements NotificationCenter, NotificationSender { + private errorReporter: ErrorReporter; + + private removerId = 1; + private eventEmitter: EventEmitter<NotificationPayload> = new EventEmitter(); + private removers: Map<number, Fn> = new Map(); + + /** + * @constructor + * @param {NotificationCenterOptions} options + * @param {LogHandler} options.logger An instance of a logger to log messages with + * @param {ErrorHandler} options.errorHandler An instance of errorHandler to handle any unexpected error + */ + constructor(options: NotificationCenterOptions) { + this.errorReporter = new ErrorReporter(options.logger, options.errorNotifier); + } + + /** + * Add a notification callback to the notification center + * @param {string} notificationType One of the values from NOTIFICATION_TYPES in utils/enums/index.js + * @param {NotificationListener<T>} callback Function that will be called when the event is triggered + * @returns {number} If the callback was successfully added, returns a listener ID which can be used + * to remove the callback by calling removeNotificationListener. The ID is a number greater than 0. + * If there was an error and the listener was not added, addNotificationListener returns -1. This + * can happen if the first argument is not a valid notification type, or if the same callback + * function was already added as a listener by a prior call to this function. + */ + addNotificationListener<N extends NotificationType>( + notificationType: N, + callback: Consumer<NotificationPayload[N]> + ): number { + const notificationTypeValues: string[] = objectValues(NOTIFICATION_TYPES); + const isNotificationTypeValid = notificationTypeValues.indexOf(notificationType) > -1; + if (!isNotificationTypeValid) { + return -1; + } + + const returnId = this.removerId++; + const remover = this.eventEmitter.on( + notificationType, this.wrapWithErrorReporting(notificationType, callback)); + this.removers.set(returnId, remover); + return returnId; + } + + private wrapWithErrorReporting<N extends NotificationType>( + notificationType: N, + callback: Consumer<NotificationPayload[N]> + ): Consumer<NotificationPayload[N]> { + return (notificationData: NotificationPayload[N]) => { + try { + callback(notificationData); + } catch (ex: any) { + const message = ex instanceof Error ? ex.message : String(ex); + this.errorReporter.report(NOTIFICATION_LISTENER_EXCEPTION, notificationType, message); + } + }; + } + + /** + * Remove a previously added notification callback + * @param {number} listenerId ID of listener to be removed + * @returns {boolean} Returns true if the listener was found and removed, and false + * otherwise. + */ + removeNotificationListener(listenerId: number): boolean { + const remover = this.removers.get(listenerId); + if (remover) { + remover(); + return true; + } + return false + } + + /** + * Removes all previously added notification listeners, for all notification types + */ + clearAllNotificationListeners(): void { + this.eventEmitter.removeAllListeners(); + } + + /** + * Remove all previously added notification listeners for the argument type + * @param {NotificationType} notificationType One of NotificationType + */ + clearNotificationListeners(notificationType: NotificationType): void { + this.eventEmitter.removeListeners(notificationType); + } + + /** + * Fires notifications for the argument type. All registered callbacks for this type will be + * called. The notificationData object will be passed on to callbacks called. + * @param {NotificationType} notificationType One of NotificationType + * @param {Object} notificationData Will be passed to callbacks called + */ + sendNotifications<N extends NotificationType>( + notificationType: N, + notificationData: NotificationPayload[N] + ): void { + this.eventEmitter.emit(notificationType, notificationData); + } +} + +/** + * Create an instance of NotificationCenter + * @param {NotificationCenterOptions} options + * @returns {NotificationCenter} An instance of NotificationCenter + */ +export function createNotificationCenter(options: NotificationCenterOptions): DefaultNotificationCenter { + return new DefaultNotificationCenter(options); +} diff --git a/lib/notification_center/type.ts b/lib/notification_center/type.ts new file mode 100644 index 000000000..01adc56e5 --- /dev/null +++ b/lib/notification_center/type.ts @@ -0,0 +1,153 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LogEvent } from '../event_processor/event_dispatcher/event_dispatcher'; +import { + EventTags, + Experiment, + FeatureVariableValue, + Holdout, + UserAttributes, + VariableType, + Variation, +} from '../shared_types'; +import { DecisionSource } from '../utils/enums'; +import { Nullable } from '../utils/type'; +import { holdout, IfActive } from '../feature_toggle'; + +export type UserEventListenerPayload = { + userId: string; + attributes?: UserAttributes; +} + +export type ActivateListenerPayload = UserEventListenerPayload & { + experiment: Experiment | null; + holdout: IfActive<typeof holdout, Holdout | null>; + variation: Variation | null; + logEvent: LogEvent; +} + +export type TrackListenerPayload = UserEventListenerPayload & { + eventKey: string; + eventTags?: EventTags; + logEvent: LogEvent; +} + +export const DECISION_NOTIFICATION_TYPES = { + AB_TEST: 'ab-test', + FEATURE: 'feature', + FEATURE_TEST: 'feature-test', + FEATURE_VARIABLE: 'feature-variable', + ALL_FEATURE_VARIABLES: 'all-feature-variables', + FLAG: 'flag', +} as const; + + +export type DecisionNotificationType = typeof DECISION_NOTIFICATION_TYPES[keyof typeof DECISION_NOTIFICATION_TYPES]; + +export type ExperimentAndVariationInfo = { + experimentKey: string; + variationKey: string; +} + +export type DecisionSourceInfo = Partial<ExperimentAndVariationInfo>; + +export type AbTestDecisonInfo = Nullable<ExperimentAndVariationInfo, 'variationKey'>; + +type FeatureDecisionInfo = { + featureKey: string, + featureEnabled: boolean, + source: DecisionSource, + sourceInfo: DecisionSourceInfo, +} + +export type FeatureTestDecisionInfo = Nullable<ExperimentAndVariationInfo, 'variationKey'>; + +export type FeatureVariableDecisionInfo = { + featureKey: string, + featureEnabled: boolean, + source: DecisionSource, + variableKey: string, + variableValue: FeatureVariableValue, + variableType: VariableType, + sourceInfo: DecisionSourceInfo, +}; + +export type VariablesMap = { [variableKey: string]: unknown } + +export type AllFeatureVariablesDecisionInfo = { + featureKey: string, + featureEnabled: boolean, + source: DecisionSource, + variableValues: VariablesMap, + sourceInfo: DecisionSourceInfo, +}; + +export type FlagDecisionInfo = { + flagKey: string, + enabled: boolean, + variationKey: string | null, + ruleKey: string | null, + variables: VariablesMap, + reasons: string[], + decisionEventDispatched: boolean, + experimentId: string | null, + variationId: string | null, +}; + +export type DecisionInfo = { + [DECISION_NOTIFICATION_TYPES.AB_TEST]: AbTestDecisonInfo; + [DECISION_NOTIFICATION_TYPES.FEATURE]: FeatureDecisionInfo; + [DECISION_NOTIFICATION_TYPES.FEATURE_TEST]: FeatureTestDecisionInfo; + [DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE]: FeatureVariableDecisionInfo; + [DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES]: AllFeatureVariablesDecisionInfo; + [DECISION_NOTIFICATION_TYPES.FLAG]: FlagDecisionInfo; +} + +export type DecisionListenerPayloadForType<T extends DecisionNotificationType> = UserEventListenerPayload & { + type: T; + decisionInfo: DecisionInfo[T]; +} + +export type DecisionListenerPayload = { + [T in DecisionNotificationType]: DecisionListenerPayloadForType<T>; +}[DecisionNotificationType]; + +export type LogEventListenerPayload = LogEvent; + +export type OptimizelyConfigUpdateListenerPayload = undefined; + +export type NotificationPayload = { + ACTIVATE: ActivateListenerPayload; + DECISION: DecisionListenerPayload; + TRACK: TrackListenerPayload; + LOG_EVENT: LogEventListenerPayload; + OPTIMIZELY_CONFIG_UPDATE: OptimizelyConfigUpdateListenerPayload; +}; + +export type NotificationType = keyof NotificationPayload; + +export type NotificationTypeValues = { + [key in NotificationType]: key; +} + +export const NOTIFICATION_TYPES: NotificationTypeValues = { + ACTIVATE: 'ACTIVATE', + DECISION: 'DECISION', + LOG_EVENT: 'LOG_EVENT', + OPTIMIZELY_CONFIG_UPDATE: 'OPTIMIZELY_CONFIG_UPDATE', + TRACK: 'TRACK', +}; diff --git a/lib/odp/constant.ts b/lib/odp/constant.ts new file mode 100644 index 000000000..c33f3f0c9 --- /dev/null +++ b/lib/odp/constant.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum ODP_USER_KEY { + VUID = 'vuid', + FS_USER_ID = 'fs_user_id', + FS_USER_ID_ALIAS = 'fs-user-id', +} + +export enum ODP_EVENT_ACTION { + IDENTIFIED = 'identified', + INITIALIZED = 'client_initialized', +} + +export const ODP_DEFAULT_EVENT_TYPE = 'fullstack'; diff --git a/lib/odp/event_manager/odp_event.ts b/lib/odp/event_manager/odp_event.ts new file mode 100644 index 000000000..062798d1b --- /dev/null +++ b/lib/odp/event_manager/odp_event.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class OdpEvent { + /** + * Type of event (typically "fullstack") + */ + type: string; + + /** + * Subcategory of the event type + */ + action: string; + + /** + * Key-value map of user identifiers + */ + identifiers: Map<string, string>; + + /** + * Event data in a key-value map + */ + data: Map<string, unknown>; + + /** + * Event to be sent and stored in the Optimizely Data Platform + * @param type Type of event (typically "fullstack") + * @param action Subcategory of the event type + * @param identifiers Key-value map of user identifiers + * @param data Event data in a key-value map. + */ + constructor(type: string, action: string, identifiers?: Map<string, string>, data?: Map<string, unknown>) { + this.type = type; + this.action = action; + this.identifiers = identifiers ?? new Map<string, string>(); + this.data = data ?? new Map<string, unknown>(); + } +} diff --git a/lib/odp/event_manager/odp_event_api_manager.spec.ts b/lib/odp/event_manager/odp_event_api_manager.spec.ts new file mode 100644 index 000000000..04d74ea18 --- /dev/null +++ b/lib/odp/event_manager/odp_event_api_manager.spec.ts @@ -0,0 +1,225 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; + +import { DefaultOdpEventApiManager, eventApiRequestGenerator, LOGGER_NAME, pixelApiRequestGenerator } from './odp_event_api_manager'; +import { OdpEvent } from './odp_event'; +import { OdpConfig } from '../odp_config'; + +const data1 = new Map<string, unknown>(); +data1.set('key11', 'value-1'); +data1.set('key12', true); +data1.set('key13', 3.5); +data1.set('key14', null); + +const data2 = new Map<string, unknown>(); + +data2.set('key2', 'value-2'); + +const ODP_EVENTS = [ + new OdpEvent('t1', 'a1', new Map([['id-key-1', 'id-value-1']]), data1), + new OdpEvent('t2', 'a2', new Map([['id-key-2', 'id-value-2']]), data2), +]; + +const API_KEY = 'test-api-key'; +const API_HOST = 'https://odp.example.com'; +const PIXEL_URL = 'https://odp.pixel.com'; + +const odpConfig = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, []); + +import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; +import { getMockLogger } from '../../tests/mock/mock_logger'; + +describe('DefaultOdpEventApiManager', () => { + it('should set name on the logger passed into the constructor', () => { + const logger = getMockLogger(); + const requestHandler = getMockRequestHandler(); + + const manager = new DefaultOdpEventApiManager(requestHandler, vi.fn(), logger); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should set name on the logger set by setLogger', () => { + const logger = getMockLogger(); + const requestHandler = getMockRequestHandler(); + + const manager = new DefaultOdpEventApiManager(requestHandler, vi.fn()); + manager.setLogger(logger); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should generate the event request using the correct odp config and event', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.resolve({ + statusCode: 200, + body: '', + headers: {}, + }), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + manager.sendEvents(odpConfig, ODP_EVENTS); + + expect(requestGenerator.mock.calls[0][0]).toEqual(odpConfig); + expect(requestGenerator.mock.calls[0][1]).toEqual(ODP_EVENTS); + }); + + it('should send the correct request using the request handler', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.resolve({ + statusCode: 200, + body: '', + headers: {}, + }), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + manager.sendEvents(odpConfig, ODP_EVENTS); + + expect(mockRequestHandler.makeRequest.mock.calls[0][0]).toEqual('https://odp.example.com/v3/events'); + expect(mockRequestHandler.makeRequest.mock.calls[0][1]).toEqual({ + 'x-api-key': 'test-api', + }); + expect(mockRequestHandler.makeRequest.mock.calls[0][2]).toEqual('PATCH'); + expect(mockRequestHandler.makeRequest.mock.calls[0][3]).toEqual('event-data'); + }); + + it('should return a promise that fails if the requestHandler response promise fails', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.reject(new Error('REQUEST_FAILED')), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + const response = manager.sendEvents(odpConfig, ODP_EVENTS); + + await expect(response).rejects.toThrow(); + }); + + it('should return a promise that resolves with correct response code from the requestHandler', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.resolve({ + statusCode: 226, + body: '', + headers: {}, + }), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + const response = manager.sendEvents(odpConfig, ODP_EVENTS); + + await expect(response).resolves.not.toThrow(); + const statusCode = await response.then((r) => r.statusCode); + expect(statusCode).toBe(226); + }); +}); + +describe('pixelApiRequestGenerator', () => { + it('should generate the correct request for the pixel API using only the first event', () => { + const request = pixelApiRequestGenerator(odpConfig, ODP_EVENTS); + expect(request.method).toBe('GET'); + const endpoint = new URL(request.endpoint); + expect(endpoint.origin).toBe(PIXEL_URL); + expect(endpoint.pathname).toBe('/v2/zaius.gif'); + expect(endpoint.searchParams.get('id-key-1')).toBe('id-value-1'); + expect(endpoint.searchParams.get('key11')).toBe('value-1'); + expect(endpoint.searchParams.get('key12')).toBe('true'); + expect(endpoint.searchParams.get('key13')).toBe('3.5'); + expect(endpoint.searchParams.get('key14')).toBe('null'); + expect(endpoint.searchParams.get('tracker_id')).toBe(API_KEY); + expect(endpoint.searchParams.get('event_type')).toBe('t1'); + expect(endpoint.searchParams.get('vdl_action')).toBe('a1'); + + expect(request.headers).toEqual({}); + expect(request.data).toBe(''); + }); +}); + +describe('eventApiRequestGenerator', () => { + it('should generate the correct request for the event API using all events', () => { + const request = eventApiRequestGenerator(odpConfig, ODP_EVENTS); + expect(request.method).toBe('POST'); + expect(request.endpoint).toBe('https://odp.example.com/v3/events'); + expect(request.headers).toEqual({ + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + }); + + const data = JSON.parse(request.data); + expect(data).toEqual([ + { + type: 't1', + action: 'a1', + identifiers: { + 'id-key-1': 'id-value-1', + }, + data: { + key11: 'value-1', + key12: true, + key13: 3.5, + key14: null, + }, + }, + { + type: 't2', + action: 'a2', + identifiers: { + 'id-key-2': 'id-value-2', + }, + data: { + key2: 'value-2', + }, + }, + ]); + }); +}); diff --git a/lib/odp/event_manager/odp_event_api_manager.ts b/lib/odp/event_manager/odp_event_api_manager.ts new file mode 100644 index 000000000..79154b06e --- /dev/null +++ b/lib/odp/event_manager/odp_event_api_manager.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerFacade } from '../../logging/logger'; +import { OdpEvent } from './odp_event'; +import { HttpMethod, RequestHandler } from '../../utils/http_request_handler/http'; +import { OdpConfig } from '../odp_config'; + +export type EventDispatchResponse = { + statusCode?: number; +}; +export interface OdpEventApiManager { + sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise<EventDispatchResponse>; + setLogger(logger: LoggerFacade): void; +} + +export type EventRequest = { + method: HttpMethod; + endpoint: string; + headers: Record<string, string>; + data: string; +} + +export type EventRequestGenerator = (odpConfig: OdpConfig, events: OdpEvent[]) => EventRequest; + +export const LOGGER_NAME = 'OdpEventApiManager'; + +export class DefaultOdpEventApiManager implements OdpEventApiManager { + private logger?: LoggerFacade; + private requestHandler: RequestHandler; + private requestGenerator: EventRequestGenerator; + + constructor( + requestHandler: RequestHandler, + requestDataGenerator: EventRequestGenerator, + logger?: LoggerFacade + ) { + this.requestHandler = requestHandler; + this.requestGenerator = requestDataGenerator; + if (logger) { + this.setLogger(logger) + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + } + + async sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise<EventDispatchResponse> { + if (events.length === 0) { + return {}; + } + + const { method, endpoint, headers, data } = this.requestGenerator(odpConfig, events); + + const request = this.requestHandler.makeRequest(endpoint, headers, method, data); + return request.responsePromise; + } +} + +export const pixelApiRequestGenerator: EventRequestGenerator = (odpConfig: OdpConfig, events: OdpEvent[]): EventRequest => { + const pixelApiPath = 'v2/zaius.gif'; + const pixelApiEndpoint = new URL(pixelApiPath, odpConfig.pixelUrl); + + const apiKey = odpConfig.apiKey; + const method = 'GET'; + const event = events[0]; + + event.identifiers.forEach((v, k) => { + pixelApiEndpoint.searchParams.append(k, v); + }); + event.data.forEach((v, k) => { + pixelApiEndpoint.searchParams.append(k, v as string); + }); + pixelApiEndpoint.searchParams.append('tracker_id', apiKey); + pixelApiEndpoint.searchParams.append('event_type', event.type); + pixelApiEndpoint.searchParams.append('vdl_action', event.action); + const endpoint = pixelApiEndpoint.toString(); + + return { + method, + endpoint, + headers: {}, + data: '', + }; +} + +export const eventApiRequestGenerator: EventRequestGenerator = (odpConfig: OdpConfig, events: OdpEvent[]): EventRequest => { + const { apiHost, apiKey } = odpConfig; + + return { + method: 'POST', + endpoint: `${apiHost}/v3/events`, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }, + data: JSON.stringify(events, (_: unknown, value: unknown) => { + return value instanceof Map ? Object.fromEntries(value) : value; + }), + }; +} diff --git a/lib/odp/event_manager/odp_event_manager.spec.ts b/lib/odp/event_manager/odp_event_manager.spec.ts new file mode 100644 index 000000000..68484d788 --- /dev/null +++ b/lib/odp/event_manager/odp_event_manager.spec.ts @@ -0,0 +1,1054 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import { DefaultOdpEventManager, LOGGER_NAME } from './odp_event_manager'; +import { getMockRepeater } from '../../tests/mock/mock_repeater'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { ServiceState } from '../../service'; +import { exhaustMicrotasks } from '../../tests/testUtils'; +import { OdpEvent } from './odp_event'; +import { OdpConfig } from '../odp_config'; +import { EventDispatchResponse } from './odp_event_api_manager'; +import { advanceTimersByTime } from '../../tests/testUtils'; + +const API_KEY = 'test-api-key'; +const API_HOST = 'https://odp.example.com'; +const PIXEL_URL = 'https://odp.pixel.com'; +const SEGMENTS_TO_CHECK = ['segment1', 'segment2']; + +const config = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, SEGMENTS_TO_CHECK); + +const makeEvent = (id: number) => { + const identifiers = new Map<string, string>(); + identifiers.set('identifier1', 'value1-' + id); + identifiers.set('identifier2', 'value2-' + id); + + const data = new Map<string, unknown>(); + data.set('data1', 'data-value1-' + id); + data.set('data2', id); + + return new OdpEvent('test-type-' + id, 'test-action-' + id, identifiers, data); +}; + +const getMockApiManager = () => { + return { + sendEvents: vi.fn(), + setLogger: vi.fn(), + }; +}; + +describe('DefaultOdpEventManager', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should be in new state after construction', () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + expect(odpEventManager.getState()).toBe(ServiceState.New); + }); + + it('should set name on the logger set using setLogger', () => { + const logger = getMockLogger(); + + const manager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + manager.setLogger(logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should pass a child logger to the event api manager when a logger is set using setLogger', () => { + const logger = getMockLogger(); + const childLogger = getMockLogger(); + logger.child.mockReturnValue(childLogger); + + const apiManager = getMockApiManager(); + + const manager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + manager.setLogger(logger); + expect(apiManager.setLogger).toHaveBeenCalledWith(childLogger); + }); + + it('should stay in starting state if started with a odpIntegationConfig and not resolve or reject onRunning', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + const onRunningHandler = vi.fn(); + odpEventManager.onRunning().then(onRunningHandler, onRunningHandler); + + odpEventManager.start(); + expect(odpEventManager.getState()).toBe(ServiceState.Starting); + + await exhaustMicrotasks(); + + expect(odpEventManager.getState()).toBe(ServiceState.Starting); + expect(onRunningHandler).not.toHaveBeenCalled(); + }); + + it('should move to running state and resolve onRunning() is start() is called after updateConfig()', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: false, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + }); + + it('should move to running state and resolve onRunning() is updateConfig() is called after start()', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.start(); + + odpEventManager.updateConfig({ + integrated: false, + }); + + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + }); + + it('should queue events until batchSize is reached', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for (let i = 0; i < 9; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + events.push(makeEvent(9)); + odpEventManager.sendEvent(events[9]); + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + }); + + it('should send events immediately asynchronously if batchSize is 1', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + for (let i = 0; i < 10; i++) { + const event = makeEvent(i); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i + 1, config, [event]); + } + }); + + it('should flush the queue immediately if disposable, regardless of the batchSize', async () => { + const apiManager = getMockApiManager(); + const repeater = getMockRepeater() + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater, + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + odpEventManager.makeDisposable(); + odpEventManager.start(); + + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const event = makeEvent(0); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, [event]); + expect(repeater.reset).toHaveBeenCalledTimes(1); + expect(repeater.start).not.toHaveBeenCalled(); + }) + + it('drops events and logs if the state is not running', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + expect(odpEventManager.getState()).toBe(ServiceState.New); + + const event = makeEvent(0); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('drops events and logs if odpIntegrationConfig is not integrated', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: false, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = makeEvent(0); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('drops event and logs if there is no identifier', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = new OdpEvent('test-type', 'test-action', new Map(), new Map()); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('accepts string, number, boolean, and null values for data', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const data = new Map<string, unknown>(); + data.set('string', 'string-value'); + data.set('number', 123); + data.set('boolean', true); + data.set('null', null); + + const event = new OdpEvent('test-type', 'test-action', new Map([['k', 'v']]), data); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, [event]); + }); + + it('should drop event and log if data contains values other than string, number, boolean, or null', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const data = new Map<string, unknown>(); + data.set('string', 'string-value'); + data.set('number', 123); + data.set('boolean', true); + data.set('null', null); + data.set('invalid', new Date()); + + const event = new OdpEvent('test-type', 'test-action', new Map([['k', 'v']]), data); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('should drop event and log if action is empty', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = new OdpEvent('test-type', '', new Map([['k', 'v']]), new Map([['k', 'v']])); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('should use fullstack as type if type is empty', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = new OdpEvent('', 'test-action', new Map([['k', 'v']]), new Map([['k', 'v']])); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents.mock.calls[0][1][0].type).toBe('fullstack'); + }); + + it('should transform identifiers with keys FS-USER-ID, fs-user-id and FS_USER_ID to fs_user_id', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 3, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event1 = new OdpEvent('test-type', 'test-action', new Map([['FS-USER-ID', 'value1']]), new Map([['k', 'v']])); + const event2 = new OdpEvent('test-type', 'test-action', new Map([['fs-user-id', 'value2']]), new Map([['k', 'v']])); + const event3 = new OdpEvent('test-type', 'test-action', new Map([['FS_USER_ID', 'value3']]), new Map([['k', 'v']])); + + odpEventManager.sendEvent(event1); + odpEventManager.sendEvent(event2); + odpEventManager.sendEvent(event3); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents.mock.calls[0][1][0].identifiers.get('fs_user_id')).toBe('value1'); + expect(apiManager.sendEvents.mock.calls[0][1][1].identifiers.get('fs_user_id')).toBe('value2'); + expect(apiManager.sendEvents.mock.calls[0][1][2].identifiers.get('fs_user_id')).toBe('value3'); + }); + + it('should start the repeater when the first event is sent', async () => { + const repeater = getMockRepeater(); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: getMockApiManager(), + batchSize: 300, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + expect(repeater.start).not.toHaveBeenCalled(); + + for(let i = 0; i < 10; i++) { + odpEventManager.sendEvent(makeEvent(i)); + await exhaustMicrotasks(); + expect(repeater.start).toHaveBeenCalledTimes(1); + } + }); + + it('should flush the queue when the repeater triggers', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + await repeater.execute(0); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + }); + + it('should reset the repeater after flush', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + expect(repeater.reset).not.toHaveBeenCalled(); + + await repeater.execute(0); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + expect(repeater.reset).toHaveBeenCalledTimes(1); + }); + + it('should retry specified number of times with backoff if apiManager.sendEvents returns a rejecting promise', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockReturnValue(Promise.reject(new Error('FAILED_TO_DISPATCH_EVENTS'))); + + const backoffController = { + backoff: vi.fn().mockReturnValue(666), + reset: vi.fn(), + }; + + const maxRetries = 5; + const retryConfig = { + maxRetries, + backoffProvider: () => backoffController, + }; + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: retryConfig, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + repeater.execute(0); + for(let i = 1; i <= maxRetries; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(666); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i, config, events); + expect(backoffController.backoff).toHaveBeenCalledTimes(i); + } + }); + + it('should retry specified number of times with backoff if apiManager returns 5xx', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockReturnValue(Promise.resolve({ statusCode: 500 })); + + const backoffController = { + backoff: vi.fn().mockReturnValue(666), + reset: vi.fn(), + }; + + const maxRetries = 5; + const retryConfig = { + maxRetries, + backoffProvider: () => backoffController, + }; + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: retryConfig, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + repeater.execute(0); + for(let i = 1; i <= maxRetries; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(666); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i, config, events); + expect(backoffController.backoff).toHaveBeenCalledTimes(i); + } + }); + + it('should log error if event sends fails even after retry', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockReturnValue(Promise.reject(new Error('FAILED_TO_DISPATCH_EVENTS'))); + + const backoffController = { + backoff: vi.fn().mockReturnValue(666), + reset: vi.fn(), + }; + + const maxRetries = 5; + const retryConfig = { + maxRetries, + backoffProvider: () => backoffController, + }; + + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: retryConfig, + }); + + odpEventManager.setLogger(logger); + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + repeater.execute(0); + for(let i = 1; i <= maxRetries; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(666); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i, config, events); + expect(backoffController.backoff).toHaveBeenCalledTimes(i); + } + + await exhaustMicrotasks(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('flushes the queue with old config if updateConfig is called with a new config', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + const newConfig = new OdpConfig('new-api-key', 'https://new-odp.example.com', 'https://new-odp.pixel.com', ['new-segment']); + odpEventManager.updateConfig({ + integrated: true, + odpConfig: newConfig, + }); + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledOnce(); + expect(apiManager.sendEvents).toHaveBeenCalledWith(config, events); + }); + + it('uses the new config after updateConfig is called', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + const newConfig = new OdpConfig('new-api-key', 'https://new-odp.example.com', 'https://new-odp.pixel.com', ['new-segment']); + odpEventManager.updateConfig({ + integrated: true, + odpConfig: newConfig, + }); + + const newEvents: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + newEvents.push(makeEvent(i + 10)); + odpEventManager.sendEvent(newEvents[i]); + } + + repeater.execute(0); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(2); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(2, newConfig, newEvents); + }); + + it('should reject onRunning() if stop() is called in new state', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.stop(); + await expect(odpEventManager.onRunning()).rejects.toThrow(); + }); + + it('should flush the queue and reset the repeater if stop() is called in running state', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + odpEventManager.stop(); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenCalledWith(config, events); + expect(repeater.reset).toHaveBeenCalledTimes(1); + }); + + it('resolve onTerminated() and go to Terminated state if stop() is called in running state', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + odpEventManager.stop(); + await expect(odpEventManager.onTerminated()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Terminated); + }); + + it('should flush the queue when flushImmediately() is called in running state', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + odpEventManager.flushImmediately(); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenCalledWith(config, events); + expect(odpEventManager.isRunning()).toBe(true); + }); +}); diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts new file mode 100644 index 000000000..d1a30d3ff --- /dev/null +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -0,0 +1,248 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OdpEvent } from './odp_event'; +import { OdpConfig, OdpIntegrationConfig } from '../odp_config'; +import { OdpEventApiManager } from './odp_event_api_manager'; +import { BaseService, Service, ServiceState, StartupLog } from '../../service'; +import { BackoffController, Repeater } from '../../utils/repeater/repeater'; +import { Producer } from '../../utils/type'; +import { runWithRetry } from '../../utils/executor/backoff_retry_runner'; +import { isSuccessStatusCode } from '../../utils/http_request_handler/http_util'; +import { ODP_DEFAULT_EVENT_TYPE, ODP_USER_KEY } from '../constant'; +import { + EVENT_ACTION_INVALID, + EVENT_DATA_INVALID, + FAILED_TO_SEND_ODP_EVENTS, + ODP_EVENTS_SHOULD_HAVE_ATLEAST_ONE_KEY_VALUE, + ODP_NOT_INTEGRATED, + FAILED_TO_DISPATCH_EVENTS, + ODP_EVENT_MANAGER_STOPPED, + SERVICE_NOT_RUNNING +} from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { LoggerFacade } from '../../logging/logger'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../../service'; +import { sprintf } from '../../utils/fns'; + +export interface OdpEventManager extends Service { + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void; + sendEvent(event: OdpEvent): void; + setLogger(logger: LoggerFacade): void; + flushImmediately(): Promise<unknown>; +} + +export type RetryConfig = { + maxRetries: number; + backoffProvider: Producer<BackoffController>; +} + +export type OdpEventManagerConfig = { + repeater: Repeater, + apiManager: OdpEventApiManager, + batchSize: number, + startUpLogs?: StartupLog[], + retryConfig: RetryConfig, +}; + +export const LOGGER_NAME = 'OdpEventManager'; + +export class DefaultOdpEventManager extends BaseService implements OdpEventManager { + private queue: OdpEvent[] = []; + private repeater: Repeater; + private odpIntegrationConfig?: OdpIntegrationConfig; + private apiManager: OdpEventApiManager; + private batchSize: number; + + private retryConfig: RetryConfig; + + constructor(config: OdpEventManagerConfig) { + super(config.startUpLogs); + + this.apiManager = config.apiManager; + this.batchSize = config.batchSize; + this.retryConfig = config.retryConfig; + + this.repeater = config.repeater; + this.repeater.setTask(() => this.flush()); + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + this.apiManager.setLogger(logger.child()); + } + + private async executeDispatch(odpConfig: OdpConfig, batch: OdpEvent[]): Promise<unknown> { + const res = await this.apiManager.sendEvents(odpConfig, batch); + if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { + return Promise.reject(new OptimizelyError(FAILED_TO_DISPATCH_EVENTS, res.statusCode)); + } + return await Promise.resolve(res); + } + + private async flush(): Promise<unknown> { + if (!this.odpIntegrationConfig || !this.odpIntegrationConfig.integrated) { + return; + } + + const odpConfig = this.odpIntegrationConfig.odpConfig; + + const batch = this.queue; + this.queue = []; + + // as the queue has been emptied, stop repeating flush + // until more events become available + this.repeater.reset(); + + return runWithRetry( + () => this.executeDispatch(odpConfig, batch), this.retryConfig.backoffProvider(), this.retryConfig.maxRetries + ).result.catch((err) => { + this.logger?.error(FAILED_TO_SEND_ODP_EVENTS, err); + }); + } + + start(): void { + if (!this.isNew()) { + return; + } + + super.start(); + + if (this.odpIntegrationConfig) { + this.goToRunningState(); + } else { + this.state = ServiceState.Starting; + } + } + + makeDisposable(): void { + super.makeDisposable(); + this.retryConfig.maxRetries = Math.min(this.retryConfig.maxRetries, 5); + this.batchSize = 1; + } + + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void { + if (this.isDone()) { + return; + } + + if (this.isNew()) { + this.odpIntegrationConfig = odpIntegrationConfig; + return; + } + + if (this.isStarting()) { + this.odpIntegrationConfig = odpIntegrationConfig; + this.goToRunningState(); + return; + } + + // already running, flush the queue using the previous config first before updating the config + this.flush(); + this.odpIntegrationConfig = odpIntegrationConfig; + } + + private goToRunningState() { + this.state = ServiceState.Running; + this.startPromise.resolve(); + } + + flushImmediately(): Promise<unknown> { + if (!this.isRunning()) { + return Promise.resolve(); + } + return this.flush(); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (this.isNew()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'OdpEventManager') + )); + } + + this.flush(); + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + } + + sendEvent(event: OdpEvent): void { + if (!this.isRunning()) { + this.logger?.error(SERVICE_NOT_RUNNING, 'OdpEventManager'); + return; + } + + if (!this.odpIntegrationConfig?.integrated) { + this.logger?.error(ODP_NOT_INTEGRATED); + return; + } + + if (event.identifiers.size === 0) { + this.logger?.error(ODP_EVENTS_SHOULD_HAVE_ATLEAST_ONE_KEY_VALUE); + return; + } + + if (!this.isDataValid(event.data)) { + this.logger?.error(EVENT_DATA_INVALID); + return; + } + + if (!event.action ) { + this.logger?.error(EVENT_ACTION_INVALID); + return; + } + + if (event.type === '') { + event.type = ODP_DEFAULT_EVENT_TYPE; + } + + Array.from(event.identifiers.entries()).forEach(([key, value]) => { + // Catch for fs-user-id, FS-USER-ID, and FS_USER_ID and assign value to fs_user_id identifier. + if ( + ODP_USER_KEY.FS_USER_ID_ALIAS === key.toLowerCase() || + ODP_USER_KEY.FS_USER_ID === key.toLowerCase() + ) { + event.identifiers.delete(key); + event.identifiers.set(ODP_USER_KEY.FS_USER_ID, value); + } + }); + + this.processEvent(event); + } + + private isDataValid(data: Map<string, any>): boolean { + const validTypes: string[] = ['string', 'number', 'boolean']; + return Array.from(data.values()).reduce( + (valid, value) => valid && (value === null || validTypes.includes(typeof value)), + true, + ); + } + + private processEvent(event: OdpEvent): void { + this.queue.push(event); + + if (this.queue.length === this.batchSize) { + this.flush(); + } else if (!this.repeater.isRunning()) { + this.repeater.start(); + } + } +} diff --git a/lib/odp/odp_config.ts b/lib/odp/odp_config.ts new file mode 100644 index 000000000..5003e1238 --- /dev/null +++ b/lib/odp/odp_config.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { checkArrayEquality } from '../utils/fns'; + +export class OdpConfig { + /** + * Host of ODP audience segments API. + */ + readonly apiHost: string; + + /** + * Public API key for the ODP account from which the audience segments will be fetched (optional). + */ + readonly apiKey: string; + + /** + * Url for sending events via pixel. + */ + readonly pixelUrl: string; + + /** + * All ODP segments used in the current datafile (associated with apiHost/apiKey). + */ + readonly segmentsToCheck: string[]; + + constructor(apiKey: string, apiHost: string, pixelUrl: string, segmentsToCheck: string[]) { + this.apiKey = apiKey; + this.apiHost = apiHost; + this.pixelUrl = pixelUrl; + this.segmentsToCheck = segmentsToCheck; + } + + /** + * Detects if there are any changes between the current and incoming ODP Configs + * @param configToCompare ODP Configuration to check self against for equality + * @returns Boolean based on if the current ODP Config is equivalent to the incoming ODP Config + */ + equals(configToCompare: OdpConfig): boolean { + return ( + this.apiHost === configToCompare.apiHost && + this.apiKey === configToCompare.apiKey && + this.pixelUrl === configToCompare.pixelUrl && + checkArrayEquality(this.segmentsToCheck, configToCompare.segmentsToCheck) + ); + } +} + +export type OdpNotIntegratedConfig = { + readonly integrated: false; +} + +export type OdpIntegratedConfig = { + readonly integrated: true; + readonly odpConfig: OdpConfig; +} + +export const odpIntegrationsAreEqual = (config1: OdpIntegrationConfig, config2: OdpIntegrationConfig): boolean => { + if (config1.integrated !== config2.integrated) { + return false; + } + + if (config1.integrated && config2.integrated) { + return config1.odpConfig.equals(config2.odpConfig); + } + + return true; +} + +export type OdpIntegrationConfig = OdpNotIntegratedConfig | OdpIntegratedConfig; diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts new file mode 100644 index 000000000..9ae0daf69 --- /dev/null +++ b/lib/odp/odp_manager.spec.ts @@ -0,0 +1,809 @@ +/** + * Copyright 2023-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, vi, expect } from 'vitest'; + + +import { DefaultOdpManager, LOGGER_NAME } from './odp_manager'; +import { ServiceState } from '../service'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { OdpConfig } from './odp_config'; +import { exhaustMicrotasks } from '../tests/testUtils'; +import { ODP_USER_KEY } from './constant'; +import { OptimizelySegmentOption } from './segment_manager/optimizely_segment_option'; +import { OdpEventManager } from './event_manager/odp_event_manager'; +import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; +import { getMockLogger } from '../tests/mock/mock_logger'; + +const keyA = 'key-a'; +const hostA = 'host-a'; +const pixelA = 'pixel-a'; +const segmentsA = ['a']; +const userA = 'fs-user-a'; + +const keyB = 'key-b'; +const hostB = 'host-b'; +const pixelB = 'pixel-b'; +const segmentsB = ['b']; +const userB = 'fs-user-b'; + +const config = new OdpConfig(keyA, hostA, pixelA, segmentsA); +const updatedConfig = new OdpConfig(keyB, hostB, pixelB, segmentsB); + +const getMockOdpEventManager = () => { + return { + start: vi.fn(), + stop: vi.fn(), + onRunning: vi.fn(), + onTerminated: vi.fn(), + getState: vi.fn(), + updateConfig: vi.fn(), + sendEvent: vi.fn(), + makeDisposable: vi.fn(), + setLogger: vi.fn(), + flushImmediately: vi.fn(), + }; +}; + +const getMockOdpSegmentManager = () => { + return { + fetchQualifiedSegments: vi.fn(), + updateConfig: vi.fn(), + setLogger: vi.fn(), + }; +}; + +describe('DefaultOdpManager', () => { + describe('a logger is passed in the constructor', () => { + it('should set name on the logger passed into the constructor', () => { + const logger = getMockLogger(); + const manager = new DefaultOdpManager({ + logger, + eventManager: getMockOdpEventManager(), + segmentManager: getMockOdpSegmentManager(), + }); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should pass different child loggers to the eventManager and segmentManager', () => { + const logger = getMockLogger(); + const eventChildLogger = getMockLogger(); + const segmentChildLogger = getMockLogger(); + + logger.child.mockReturnValueOnce(eventChildLogger) + .mockReturnValueOnce(segmentChildLogger); + + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); + + const manager = new DefaultOdpManager({ + logger, + eventManager, + segmentManager, + }); + + expect(eventManager.setLogger).toHaveBeenCalledWith(eventChildLogger); + expect(segmentManager.setLogger).toHaveBeenCalledWith(segmentChildLogger); + }); + }); + + describe('setLogger method', () => { + it('should set name on the logger', () => { + const logger = getMockLogger(); + const manager = new DefaultOdpManager({ + eventManager: getMockOdpEventManager(), + segmentManager: getMockOdpSegmentManager(), + }); + + manager.setLogger(logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should pass a child logger to the datafileManager', () => { + const logger = getMockLogger(); + const eventChildLogger = getMockLogger(); + const segmentChildLogger = getMockLogger(); + + logger.child.mockReturnValueOnce(eventChildLogger) + .mockReturnValueOnce(segmentChildLogger); + + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); + + const manager = new DefaultOdpManager({ + eventManager, + segmentManager, + }); + manager.setLogger(logger); + + expect(eventManager.setLogger).toHaveBeenCalledWith(eventChildLogger); + expect(segmentManager.setLogger).toHaveBeenCalledWith(segmentChildLogger); + }); + }); + + it('should be in new state on construction', () => { + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager: getMockOdpEventManager(), + }); + + expect(odpManager.getState()).toEqual(ServiceState.New); + }); + + it('should be in starting state after start is called', () => { + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager: getMockOdpEventManager(), + }); + + odpManager.start(); + + expect(odpManager.getState()).toEqual(ServiceState.Starting); + }); + + it('should start eventManager after start is called', () => { + const eventManager = getMockOdpEventManager(); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(eventManager.start).toHaveBeenCalled(); + }); + + it('should stay in starting state if updateConfig is called but eventManager is still not running', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(resolvablePromise<void>().promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + }); + + it('should stay in starting state if eventManager is running but config is not yet available', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + }); + + it('should go to running state and resolve onRunning() if updateConfig is called and eventManager is running', async () => { + const eventManager = getMockOdpEventManager(); + const eventManagerPromise = resolvablePromise<void>(); + eventManager.onRunning.mockReturnValue(eventManagerPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await exhaustMicrotasks(); + + expect(odpManager.getState()).toEqual(ServiceState.Starting); + eventManagerPromise.resolve(); + + await expect(odpManager.onRunning()).resolves.not.toThrow(); + expect(odpManager.getState()).toEqual(ServiceState.Running); + }); + + it('should go to failed state and reject onRunning(), onTerminated() if updateConfig is called and eventManager fails to start', async () => { + const eventManager = getMockOdpEventManager(); + const eventManagerPromise = resolvablePromise<void>(); + eventManager.onRunning.mockReturnValue(eventManagerPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await exhaustMicrotasks(); + + expect(odpManager.getState()).toEqual(ServiceState.Starting); + eventManagerPromise.reject(new Error("Failed to start")); + + await expect(odpManager.onRunning()).rejects.toThrow(); + await expect(odpManager.onTerminated()).rejects.toThrow(); + expect(odpManager.getState()).toEqual(ServiceState.Failed); + }); + + it('should go to failed state and reject onRunning(), onTerminated() if eventManager fails to start before updateSettings()', async () => { + const eventManager = getMockOdpEventManager(); + const eventManagerPromise = resolvablePromise<void>(); + eventManager.onRunning.mockReturnValue(eventManagerPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + + eventManagerPromise.reject(new Error("Failed to start")); + + await expect(odpManager.onRunning()).rejects.toThrow(); + await expect(odpManager.onTerminated()).rejects.toThrow(); + expect(odpManager.getState()).toEqual(ServiceState.Failed); + }); + + it('should pass the changed config to eventManager and segmentManager', async () => { + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); + + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + expect(eventManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); + expect(segmentManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); + + odpManager.updateConfig({ integrated: true, odpConfig: updatedConfig }); + + expect(eventManager.updateConfig).toHaveBeenNthCalledWith(2, { integrated: true, odpConfig: updatedConfig }); + expect(segmentManager.updateConfig).toHaveBeenNthCalledWith(2, { integrated: true, odpConfig: updatedConfig }); + expect(eventManager.updateConfig).toHaveBeenCalledTimes(2); + expect(segmentManager.updateConfig).toHaveBeenCalledTimes(2); + }); + + it('should not call eventManager and segmentManager updateConfig if config does not change', async () => { + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); + + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + expect(eventManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); + expect(segmentManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); + + odpManager.updateConfig({ integrated: true, odpConfig: JSON.parse(JSON.stringify(config)) }); + + expect(eventManager.updateConfig).toHaveBeenCalledTimes(1); + expect(segmentManager.updateConfig).toHaveBeenCalledTimes(1); + }); + + it('fetches qualified segments correctly for both fs_user_id and vuid from segmentManager', async () => { + const segmentManager = getMockOdpSegmentManager(); + segmentManager.fetchQualifiedSegments.mockImplementation((key: ODP_USER_KEY) => { + if (key === ODP_USER_KEY.FS_USER_ID) { + return Promise.resolve(['fs1', 'fs2']); + } + return Promise.resolve(['vuid1', 'vuid2']); + }); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager: getMockOdpEventManager(), + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const fsSegments = await odpManager.fetchQualifiedSegments(userA); + expect(fsSegments).toEqual(['fs1', 'fs2']); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(1, ODP_USER_KEY.FS_USER_ID, userA, []); + + const vuidSegments = await odpManager.fetchQualifiedSegments('vuid_abcd'); + expect(vuidSegments).toEqual(['vuid1', 'vuid2']); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(2, ODP_USER_KEY.VUID, 'vuid_abcd', []); + }); + + it('returns null from fetchQualifiedSegments if segmentManger returns null', async () => { + const segmentManager = getMockOdpSegmentManager(); + segmentManager.fetchQualifiedSegments.mockResolvedValue(null); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager: getMockOdpEventManager(), + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const fsSegments = await odpManager.fetchQualifiedSegments(userA); + expect(fsSegments).toBeNull(); + + const vuidSegments = await odpManager.fetchQualifiedSegments('vuid_abcd'); + expect(vuidSegments).toBeNull(); + }); + + it('passes options to segmentManager correctly', async () => { + const segmentManager = getMockOdpSegmentManager(); + segmentManager.fetchQualifiedSegments.mockResolvedValue(null); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager: getMockOdpEventManager(), + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const options = [OptimizelySegmentOption.IGNORE_CACHE, OptimizelySegmentOption.RESET_CACHE]; + await odpManager.fetchQualifiedSegments(userA, options); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(1, ODP_USER_KEY.FS_USER_ID, userA, options); + + await odpManager.fetchQualifiedSegments('vuid_abcd', options); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(2, ODP_USER_KEY.VUID, 'vuid_abcd', options); + + await odpManager.fetchQualifiedSegments(userA, [OptimizelySegmentOption.IGNORE_CACHE]); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith( + 3, ODP_USER_KEY.FS_USER_ID, userA, [OptimizelySegmentOption.IGNORE_CACHE]); + + await odpManager.fetchQualifiedSegments('vuid_abcd', []); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(4, ODP_USER_KEY.VUID, 'vuid_abcd', []); + }); + + it('sends a client_intialized event with the vuid after becoming ready if setVuid is called and odp is integrated', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.setVuid('vuid_123'); + + await exhaustMicrotasks(); + expect(eventManager.sendEvent).not.toHaveBeenCalled(); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + expect(mockSendEvents).toHaveBeenCalledOnce(); + + const { type, action, identifiers } = mockSendEvents.mock.calls[0][0]; + expect(type).toEqual('fullstack'); + expect(action).toEqual('client_initialized'); + expect(identifiers).toEqual(new Map([['vuid', 'vuid_123']])); + }); + + it('does not send a client_intialized event with the vuid after becoming ready if setVuid is called and odp is not integrated', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.setVuid('vuid_123'); + + await exhaustMicrotasks(); + expect(eventManager.sendEvent).not.toHaveBeenCalled(); + + odpManager.updateConfig({ integrated: false }); + await odpManager.onRunning(); + + await exhaustMicrotasks(); + expect(mockSendEvents).not.toHaveBeenCalled(); + }); + + it('includes the available vuid in events sent via sendEvent', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.setVuid('vuid_123'); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['email', 'a@b.com'], ['vuid', 'vuid_123']])); + }); + + it('does not override the vuid in events sent via sendEvent', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.setVuid('vuid_123'); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com'], ['vuid', 'vuid_456']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['email', 'a@b.com'], ['vuid', 'vuid_456']])); + }); + + it('augments the data with common data before sending the event', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { data } = mockSendEvents.mock.calls[0][0]; + expect(data.get('idempotence_id')).toBeDefined(); + expect(data.get('data_source_type')).toEqual('sdk'); + expect(data.get('data_source')).toEqual(JAVASCRIPT_CLIENT_ENGINE); + expect(data.get('data_source_version')).toEqual(CLIENT_VERSION); + expect(data.get('key1')).toEqual('value1'); + expect(data.get('key2')).toEqual('value2'); + }); + + it('uses the clientInfo provided by setClientInfo() when augmenting the data', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.setClientInfo('client', 'version'); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { data } = mockSendEvents.mock.calls[0][0]; + expect(data.get('data_source')).toEqual('client'); + expect(data.get('data_source_version')).toEqual('version'); + }); + + it('augments the data with user agent data before sending the event if userAgentParser is provided ', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + userAgentParser: { + parseUserAgentInfo: () => ({ + os: { name: 'os', version: '1.0' }, + device: { type: 'phone', model: 'model' }, + }), + }, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { data } = mockSendEvents.mock.calls[0][0]; + expect(data.get('os')).toEqual('os'); + expect(data.get('os_version')).toEqual('1.0'); + expect(data.get('device_type')).toEqual('phone'); + expect(data.get('model')).toEqual('model'); + }); + + it('sends identified event with both fs_user_id and vuid if both parameters are provided', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.identifyUser('user', 'vuid_a'); + expect(mockSendEvents).toHaveBeenCalledOnce(); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['fs_user_id', 'user'], ['vuid', 'vuid_a']])); + }); + + it('sends identified event when called with just fs_user_id in first parameter', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.identifyUser('user'); + expect(mockSendEvents).toHaveBeenCalledOnce(); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['fs_user_id', 'user']])); + }); + + it('sends identified event when called with just vuid in first parameter', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.identifyUser('vuid_a'); + expect(mockSendEvents).toHaveBeenCalledOnce(); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['vuid', 'vuid_a']])); + }); + + it('should reject onRunning() if stopped in new state', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.stop(); + + await expect(odpManager.onRunning()).rejects.toThrow(); + }); + + it('should reject onRunning() if stopped in starting state', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + + odpManager.stop(); + await expect(odpManager.onRunning()).rejects.toThrow(); + }); + + it('should go to stopping state and wait for eventManager to stop if stop is called', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(resolvablePromise().promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.stop(); + + const terminatedHandler = vi.fn(); + odpManager.onTerminated().then(terminatedHandler); + + expect(odpManager.getState()).toEqual(ServiceState.Stopping); + await exhaustMicrotasks(); + expect(terminatedHandler).not.toHaveBeenCalled(); + }); + + it('should stop eventManager if stop is called', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + + odpManager.stop(); + expect(eventManager.stop).toHaveBeenCalled(); + }); + + it('should resolve onTerminated after eventManager stops successfully', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + const eventManagerTerminatedPromise = resolvablePromise<void>(); + eventManager.onTerminated.mockReturnValue(eventManagerTerminatedPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.stop(); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Stopping); + + eventManagerTerminatedPromise.resolve(); + await expect(odpManager.onTerminated()).resolves.not.toThrow(); + }); + + it('should reject onTerminated after eventManager fails to stop correctly', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + const eventManagerTerminatedPromise = resolvablePromise<void>(); + eventManager.onTerminated.mockReturnValue(eventManagerTerminatedPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.stop(); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Stopping); + + eventManagerTerminatedPromise.reject(new Error('FAILED_TO_STOP')); + await expect(odpManager.onTerminated()).rejects.toThrow(); + }); + + it('should call makeDisposable() on eventManager when makeDisposable() is called on odpManager', async () => { + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); + + odpManager.makeDisposable(); + + expect(eventManager.makeDisposable).toHaveBeenCalled(); + }); + + it('should call flushImmediately() on eventManager when flushImmediately() is called on odpManager', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockResolvedValue({}); + const segmentManager = getMockOdpSegmentManager(); + + eventManager.flushImmediately.mockResolvedValue({}); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + odpManager.start(); + + await odpManager.onRunning(); + + odpManager.flushImmediately(); + + expect(eventManager.flushImmediately).toHaveBeenCalledOnce(); + expect(odpManager.isRunning()).toBe(true); + }) +}); + diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts new file mode 100644 index 000000000..feaca24b9 --- /dev/null +++ b/lib/odp/odp_manager.ts @@ -0,0 +1,265 @@ +/** + * Copyright 2023-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { v4 as uuidV4} from 'uuid'; +import { LoggerFacade } from '../logging/logger'; + +import { OdpIntegrationConfig, odpIntegrationsAreEqual } from './odp_config'; +import { OdpEventManager } from './event_manager/odp_event_manager'; +import { OdpSegmentManager } from './segment_manager/odp_segment_manager'; +import { OptimizelySegmentOption } from './segment_manager/optimizely_segment_option'; +import { OdpEvent } from './event_manager/odp_event'; +import { resolvablePromise, ResolvablePromise } from '../utils/promise/resolvablePromise'; +import { BaseService, Service, ServiceState } from '../service'; +import { UserAgentParser } from './ua_parser/user_agent_parser'; +import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; +import { ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION, ODP_USER_KEY } from './constant'; +import { isVuid } from '../vuid/vuid'; +import { Maybe } from '../utils/type'; +import { sprintf } from '../utils/fns'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; + +export interface OdpManager extends Service { + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean; + fetchQualifiedSegments(userId: string, options?: Array<OptimizelySegmentOption>): Promise<string[] | null>; + identifyUser(userId: string, vuid?: string): void; + sendEvent(event: OdpEvent): void; + setClientInfo(clientEngine: string, clientVersion: string): void; + setVuid(vuid: string): void; + setLogger(logger: LoggerFacade): void; + flushImmediately(): Promise<unknown>; +} + +export type OdpManagerConfig = { + segmentManager: OdpSegmentManager; + eventManager: OdpEventManager; + logger?: LoggerFacade; + userAgentParser?: UserAgentParser; +}; + +export const LOGGER_NAME = 'OdpManager'; + +export class DefaultOdpManager extends BaseService implements OdpManager { + private configPromise: ResolvablePromise<void>; + private segmentManager: OdpSegmentManager; + private eventManager: OdpEventManager; + private odpIntegrationConfig?: OdpIntegrationConfig; + private vuid?: string; + private clientEngine = JAVASCRIPT_CLIENT_ENGINE; + private clientVersion = CLIENT_VERSION; + private userAgentData?: Map<string, unknown>; + + constructor(config: OdpManagerConfig) { + super(); + this.segmentManager = config.segmentManager; + this.eventManager = config.eventManager; + + this.configPromise = resolvablePromise(); + + if (config.userAgentParser) { + const { os, device } = config.userAgentParser.parseUserAgentInfo(); + + const userAgentInfo: Record<string, unknown> = { + 'os': os.name, + 'os_version': os.version, + 'device_type': device.type, + 'model': device.model, + }; + + this.userAgentData = new Map<string, unknown>( + Object.entries(userAgentInfo).filter(([_, value]) => value != null && value != undefined) + ); + } + + if (config.logger) { + this.setLogger(config.logger); + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + this.eventManager.setLogger(logger.child()); + this.segmentManager.setLogger(logger.child()); + } + + setClientInfo(clientEngine: string, clientVersion: string): void { + this.clientEngine = clientEngine; + this.clientVersion = clientVersion; + } + + start(): void { + if (!this.isNew()) { + return; + } + + this.state = ServiceState.Starting; + + this.eventManager.start(); + + const startDependencies = [ + this.configPromise, + this.eventManager.onRunning(), + ]; + + Promise.all(startDependencies) + .then(() => { + this.handleStartSuccess(); + }).catch((err) => { + this.handleStartFailure(err); + }); + } + + makeDisposable(): void { + super.makeDisposable(); + this.eventManager.makeDisposable(); + } + + private handleStartSuccess() { + if (this.isDone()) { + return; + } + this.state = ServiceState.Running; + this.startPromise.resolve(); + } + + private handleStartFailure(error: Error) { + if (this.isDone()) { + return; + } + + this.state = ServiceState.Failed; + this.startPromise.reject(error); + this.stopPromise.reject(error); + } + + flushImmediately(): Promise<unknown> { + if (!this.isRunning()) { + return Promise.resolve(); + } + return this.eventManager.flushImmediately(); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (!this.isRunning()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'OdpManager') + )); + } + + this.state = ServiceState.Stopping; + this.eventManager.stop(); + + this.eventManager.onTerminated().then(() => { + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + }).catch((err) => { + this.state = ServiceState.Failed; + this.stopPromise.reject(err); + }); + } + + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean { + // do nothing if config did not change + if (this.odpIntegrationConfig && odpIntegrationsAreEqual(this.odpIntegrationConfig, odpIntegrationConfig)) { + return false; + } + + if (this.isDone()) { + return false; + } + + this.odpIntegrationConfig = odpIntegrationConfig; + this.configPromise.resolve(); + this.segmentManager.updateConfig(odpIntegrationConfig) + this.eventManager.updateConfig(odpIntegrationConfig); + + return true; + } + + /** + * Attempts to fetch and return a list of a user's qualified segments from the local segments cache. + * If no cached data exists for the target user, this fetches and caches data from the ODP server instead. + * @param {string} userId - Unique identifier of a target user. + * @param {Array<OptimizelySegmentOption>} options - An array of OptimizelySegmentOption used to ignore and/or reset the cache. + * @returns {Promise<string[] | null>} A promise holding either a list of qualified segments or null. + */ + async fetchQualifiedSegments(userId: string, options: Array<OptimizelySegmentOption> = []): Promise<string[] | null> { + if (isVuid(userId)) { + return this.segmentManager.fetchQualifiedSegments(ODP_USER_KEY.VUID, userId, options); + } + + return this.segmentManager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, userId, options); + } + + identifyUser(userId: string, vuid?: string): void { + const identifiers = new Map<string, string>(); + + let finalUserId: Maybe<string> = userId; + let finalVuid: Maybe<string> = vuid; + + if (!vuid && isVuid(userId)) { + finalVuid = userId; + finalUserId = undefined; + } + + if (finalVuid) { + identifiers.set(ODP_USER_KEY.VUID, finalVuid); + } + + if (finalUserId) { + identifiers.set(ODP_USER_KEY.FS_USER_ID, finalUserId); + } + + const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); + this.sendEvent(event); + } + + sendEvent(event: OdpEvent): void { + if (!event.identifiers.has(ODP_USER_KEY.VUID) && this.vuid) { + event.identifiers.set(ODP_USER_KEY.VUID, this.vuid); + } + + event.data = this.augmentCommonData(event.data); + this.eventManager.sendEvent(event); + } + + private augmentCommonData(sourceData: Map<string, unknown>): Map<string, unknown> { + const data = new Map<string, unknown>(this.userAgentData); + + data.set('idempotence_id', uuidV4()); + data.set('data_source_type', 'sdk'); + data.set('data_source', this.clientEngine); + data.set('data_source_version', this.clientVersion); + + sourceData.forEach((value, key) => data.set(key, value)); + return data; + } + + setVuid(vuid: string): void { + this.vuid = vuid; + this.onRunning().then(() => { + if (this.odpIntegrationConfig?.integrated) { + const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.INITIALIZED); + this.sendEvent(event); + } + }); + } +} diff --git a/lib/odp/odp_manager_factory.browser.spec.ts b/lib/odp/odp_manager_factory.browser.spec.ts new file mode 100644 index 000000000..75edcdf3d --- /dev/null +++ b/lib/odp/odp_manager_factory.browser.spec.ts @@ -0,0 +1,127 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +vi.mock('../utils/http_request_handler/request_handler.browser', () => { + return { BrowserRequestHandler: vi.fn() }; +}); + +vi.mock('./odp_manager_factory', () => { + return { + getOdpManager: vi.fn().mockImplementation(() => ({})), + getOpaqueOdpManager: vi.fn().mockImplementation(() => ({})) + }; +}); + + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getOpaqueOdpManager, OdpManagerOptions } from './odp_manager_factory'; +import { BROWSER_DEFAULT_API_TIMEOUT, BROWSER_DEFAULT_BATCH_SIZE, BROWSER_DEFAULT_FLUSH_INTERVAL, createOdpManager } from './odp_manager_factory.browser'; +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; +import { eventApiRequestGenerator, pixelApiRequestGenerator } from './event_manager/odp_event_api_manager'; + +describe('createOdpManager', () => { + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const mockGetOpaqueOdpManager = vi.mocked(getOpaqueOdpManager); + + beforeEach(() => { + MockBrowserRequestHandler.mockClear(); + mockGetOpaqueOdpManager.mockClear(); + }); + + it('should use BrowserRequestHandler with the provided timeout as the segment request handler', () => { + const odpManager = createOdpManager({ segmentsApiTimeout: 3456 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(3456); + }); + + it('should use BrowserRequestHandler with the browser default timeout as the segment request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(BROWSER_DEFAULT_API_TIMEOUT); + }); + + it('should use BrowserRequestHandler with the provided timeout as the event request handler', () => { + const odpManager = createOdpManager({ eventApiTimeout: 2345 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(2345); + }); + + it('should use BrowserRequestHandler with the browser default timeout as the event request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(BROWSER_DEFAULT_API_TIMEOUT); + }); + + it('should use the provided eventBatchSize', () => { + const odpManager = createOdpManager({ eventBatchSize: 99 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(99); + }); + + it('should use the browser default eventBatchSize if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(BROWSER_DEFAULT_BATCH_SIZE); + }); + + it('should use the provided eventFlushInterval', () => { + const odpManager = createOdpManager({ eventFlushInterval: 9999 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(9999); + }); + + it('should use the browser default eventFlushInterval if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(BROWSER_DEFAULT_FLUSH_INTERVAL); + }); + + it('uses the event api request generator', () => { + const odpManager = createOdpManager({ }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestGenerator } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestGenerator).toBe(eventApiRequestGenerator); + }); + + it('uses the passed options for relevant fields', () => { + const options: OdpManagerOptions = { + segmentsCache: {} as any, + segmentsCacheSize: 11, + segmentsCacheTimeout: 2025, + eventFlushInterval: 2222, + userAgentParser: {} as any, + }; + const odpManager = createOdpManager(options); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + expect(mockGetOpaqueOdpManager).toHaveBeenNthCalledWith(1, expect.objectContaining(options)); + }); +}); diff --git a/lib/odp/odp_manager_factory.browser.ts b/lib/odp/odp_manager_factory.browser.ts new file mode 100644 index 000000000..e5d97d8e1 --- /dev/null +++ b/lib/odp/odp_manager_factory.browser.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + +export const BROWSER_DEFAULT_API_TIMEOUT = 10_000; +export const BROWSER_DEFAULT_BATCH_SIZE = 10; +export const BROWSER_DEFAULT_FLUSH_INTERVAL = 1000; + +export const createOdpManager = (options: OdpManagerOptions = {}): OpaqueOdpManager => { + const segmentRequestHandler = new BrowserRequestHandler({ + timeout: options.segmentsApiTimeout || BROWSER_DEFAULT_API_TIMEOUT, + }); + + const eventRequestHandler = new BrowserRequestHandler({ + timeout: options.eventApiTimeout || BROWSER_DEFAULT_API_TIMEOUT, + }); + + return getOpaqueOdpManager({ + ...options, + eventBatchSize: options.eventBatchSize || BROWSER_DEFAULT_BATCH_SIZE, + eventFlushInterval: options.eventFlushInterval || BROWSER_DEFAULT_FLUSH_INTERVAL, + segmentRequestHandler, + eventRequestHandler, + eventRequestGenerator: eventApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_manager_factory.node.spec.ts b/lib/odp/odp_manager_factory.node.spec.ts new file mode 100644 index 000000000..ac3bcb4ce --- /dev/null +++ b/lib/odp/odp_manager_factory.node.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +vi.mock('../utils/http_request_handler/request_handler.node', () => { + return { NodeRequestHandler: vi.fn() }; +}); + +vi.mock('./odp_manager_factory', () => { + return { + getOdpManager: vi.fn().mockImplementation(() => ({})), + getOpaqueOdpManager: vi.fn().mockImplementation(() => ({})) + }; +}); + + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getOpaqueOdpManager, OdpManagerOptions } from './odp_manager_factory'; +import { NODE_DEFAULT_API_TIMEOUT, NODE_DEFAULT_BATCH_SIZE, NODE_DEFAULT_FLUSH_INTERVAL, createOdpManager } from './odp_manager_factory.node'; +import { NodeRequestHandler } from '../utils/http_request_handler/request_handler.node'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; + +describe('createOdpManager', () => { + const MockNodeRequestHandler = vi.mocked(NodeRequestHandler); + const mockGetOpaqueOdpManager = vi.mocked(getOpaqueOdpManager); + + beforeEach(() => { + MockNodeRequestHandler.mockClear(); + mockGetOpaqueOdpManager.mockClear(); + }); + + it('should use NodeRequestHandler with the provided timeout as the segment request handler', () => { + const odpManager = createOdpManager({ segmentsApiTimeout: 3456 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockNodeRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(3456); + }); + + it('should use NodeRequestHandler with the node default timeout as the segment request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockNodeRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(NODE_DEFAULT_API_TIMEOUT); + }); + + it('should use NodeRequestHandler with the provided timeout as the event request handler', () => { + const odpManager = createOdpManager({ eventApiTimeout: 2345 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockNodeRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(2345); + }); + + it('should use NodeRequestHandler with the node default timeout as the event request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockNodeRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(NODE_DEFAULT_API_TIMEOUT); + }); + + it('uses the event api request generator', () => { + const odpManager = createOdpManager({ }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestGenerator } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestGenerator).toBe(eventApiRequestGenerator); + }); + + it('should use the provided eventBatchSize', () => { + const odpManager = createOdpManager({ eventBatchSize: 99 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(99); + }); + + it('should use the node default eventBatchSize if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(NODE_DEFAULT_BATCH_SIZE); + }); + + it('should use the provided eventFlushInterval', () => { + const odpManager = createOdpManager({ eventFlushInterval: 9999 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(9999); + }); + + it('should use the node default eventFlushInterval if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(NODE_DEFAULT_FLUSH_INTERVAL); + }); + + it('uses the passed options for relevant fields', () => { + const options: OdpManagerOptions = { + segmentsCache: {} as any, + segmentsCacheSize: 11, + segmentsCacheTimeout: 2025, + userAgentParser: {} as any, + }; + const odpManager = createOdpManager(options); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + expect(mockGetOpaqueOdpManager).toHaveBeenNthCalledWith(1, expect.objectContaining(options)); + }); +}); diff --git a/lib/odp/odp_manager_factory.node.ts b/lib/odp/odp_manager_factory.node.ts new file mode 100644 index 000000000..7b8f737a7 --- /dev/null +++ b/lib/odp/odp_manager_factory.node.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NodeRequestHandler } from '../utils/http_request_handler/request_handler.node'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + +export const NODE_DEFAULT_API_TIMEOUT = 10_000; +export const NODE_DEFAULT_BATCH_SIZE = 10; +export const NODE_DEFAULT_FLUSH_INTERVAL = 1000; + +export const createOdpManager = (options: OdpManagerOptions = {}): OpaqueOdpManager => { + const segmentRequestHandler = new NodeRequestHandler({ + timeout: options.segmentsApiTimeout || NODE_DEFAULT_API_TIMEOUT, + }); + + const eventRequestHandler = new NodeRequestHandler({ + timeout: options.eventApiTimeout || NODE_DEFAULT_API_TIMEOUT, + }); + + return getOpaqueOdpManager({ + ...options, + segmentRequestHandler, + eventRequestHandler, + eventBatchSize: options.eventBatchSize || NODE_DEFAULT_BATCH_SIZE, + eventFlushInterval: options.eventFlushInterval || NODE_DEFAULT_FLUSH_INTERVAL, + eventRequestGenerator: eventApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_manager_factory.react_native.spec.ts b/lib/odp/odp_manager_factory.react_native.spec.ts new file mode 100644 index 000000000..95d7be4fc --- /dev/null +++ b/lib/odp/odp_manager_factory.react_native.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +vi.mock('../utils/http_request_handler/request_handler.browser', () => { + return { BrowserRequestHandler: vi.fn() }; +}); + +vi.mock('./odp_manager_factory', () => { + return { + getOdpManager: vi.fn().mockImplementation(() => ({})), + getOpaqueOdpManager: vi.fn().mockImplementation(() => ({})), + }; +}); + + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getOpaqueOdpManager, OdpManagerOptions } from './odp_manager_factory'; +import { RN_DEFAULT_API_TIMEOUT, RN_DEFAULT_BATCH_SIZE, RN_DEFAULT_FLUSH_INTERVAL, createOdpManager } from './odp_manager_factory.react_native'; +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser' +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; + +describe('createOdpManager', () => { + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const mockGetOpaqueOdpManager = vi.mocked(getOpaqueOdpManager); + + beforeEach(() => { + MockBrowserRequestHandler.mockClear(); + mockGetOpaqueOdpManager.mockClear(); + }); + + it('should use BrowserRequestHandler with the provided timeout as the segment request handler', () => { + const odpManager = createOdpManager({ segmentsApiTimeout: 3456 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(3456); + }); + + it('should use BrowserRequestHandler with the node default timeout as the segment request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(RN_DEFAULT_API_TIMEOUT); + }); + + it('should use BrowserRequestHandler with the provided timeout as the event request handler', () => { + const odpManager = createOdpManager({ eventApiTimeout: 2345 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(2345); + }); + + it('should use BrowserRequestHandler with the node default timeout as the event request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(RN_DEFAULT_API_TIMEOUT); + }); + + it('uses the event api request generator', () => { + const odpManager = createOdpManager({ }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventRequestGenerator } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventRequestGenerator).toBe(eventApiRequestGenerator); + }); + + it('should use the provided eventBatchSize', () => { + const odpManager = createOdpManager({ eventBatchSize: 99 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(99); + }); + + it('should use the react_native default eventBatchSize if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(RN_DEFAULT_BATCH_SIZE); + }); + + it('should use the provided eventFlushInterval', () => { + const odpManager = createOdpManager({ eventFlushInterval: 9999 }); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(9999); + }); + + it('should use the react_native default eventFlushInterval if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOpaqueOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(RN_DEFAULT_FLUSH_INTERVAL); + }); + + it('uses the passed options for relevant fields', () => { + const options: OdpManagerOptions = { + segmentsCache: {} as any, + segmentsCacheSize: 11, + segmentsCacheTimeout: 2025, + userAgentParser: {} as any, + }; + const odpManager = createOdpManager(options); + expect(odpManager).toBe(mockGetOpaqueOdpManager.mock.results[0].value); + expect(mockGetOpaqueOdpManager).toHaveBeenNthCalledWith(1, expect.objectContaining(options)); + }); +}); diff --git a/lib/odp/odp_manager_factory.react_native.ts b/lib/odp/odp_manager_factory.react_native.ts new file mode 100644 index 000000000..c76312d6d --- /dev/null +++ b/lib/odp/odp_manager_factory.react_native.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { OdpManager } from './odp_manager'; +import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + +export const RN_DEFAULT_API_TIMEOUT = 10_000; +export const RN_DEFAULT_BATCH_SIZE = 10; +export const RN_DEFAULT_FLUSH_INTERVAL = 1000; + +export const createOdpManager = (options: OdpManagerOptions = {}): OpaqueOdpManager => { + const segmentRequestHandler = new BrowserRequestHandler({ + timeout: options.segmentsApiTimeout || RN_DEFAULT_API_TIMEOUT, + }); + + const eventRequestHandler = new BrowserRequestHandler({ + timeout: options.eventApiTimeout || RN_DEFAULT_API_TIMEOUT, + }); + + return getOpaqueOdpManager({ + ...options, + segmentRequestHandler, + eventRequestHandler, + eventBatchSize: options.eventBatchSize || RN_DEFAULT_BATCH_SIZE, + eventFlushInterval: options.eventFlushInterval || RN_DEFAULT_FLUSH_INTERVAL, + eventRequestGenerator: eventApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_manager_factory.spec.ts b/lib/odp/odp_manager_factory.spec.ts new file mode 100644 index 000000000..b80689b79 --- /dev/null +++ b/lib/odp/odp_manager_factory.spec.ts @@ -0,0 +1,415 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getMockSyncCache } from '../tests/mock/mock_cache'; + +vi.mock('./odp_manager', () => { + return { + DefaultOdpManager: vi.fn(), + }; +}); + +vi.mock('./segment_manager/odp_segment_manager', () => { + return { + DefaultOdpSegmentManager: vi.fn(), + }; +}); + +vi.mock('./segment_manager/odp_segment_api_manager', () => { + return { + DefaultOdpSegmentApiManager: vi.fn(), + }; +}); + +vi.mock('../utils/cache/in_memory_lru_cache', () => { + return { + InMemoryLruCache: vi.fn(), + }; +}); + +vi.mock('./event_manager/odp_event_manager', () => { + return { + DefaultOdpEventManager: vi.fn(), + }; +}); + +vi.mock('./event_manager/odp_event_api_manager', () => { + return { + DefaultOdpEventApiManager: vi.fn(), + }; +}); + +vi.mock( '../utils/repeater/repeater', () => { + return { + IntervalRepeater: vi.fn(), + ExponentialBackoff: vi.fn(), + }; +}); + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { DefaultOdpManager } from './odp_manager'; +import { DEFAULT_CACHE_SIZE, DEFAULT_CACHE_TIMEOUT, DEFAULT_EVENT_BATCH_SIZE, DEFAULT_EVENT_MAX_BACKOFF, DEFAULT_EVENT_MAX_RETRIES, DEFAULT_EVENT_MIN_BACKOFF, getOdpManager } from './odp_manager_factory'; +import { getMockRequestHandler } from '../tests/mock/mock_request_handler'; +import { DefaultOdpSegmentManager } from './segment_manager/odp_segment_manager'; +import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; +import { InMemoryLruCache } from '../utils/cache/in_memory_lru_cache'; +import { DefaultOdpEventManager } from './event_manager/odp_event_manager'; +import { DefaultOdpEventApiManager } from './event_manager/odp_event_api_manager'; +import { IntervalRepeater } from '../utils/repeater/repeater'; +import { ExponentialBackoff } from '../utils/repeater/repeater'; + +describe('getOdpManager', () => { + const MockDefaultOdpManager = vi.mocked(DefaultOdpManager); + const MockDefaultOdpSegmentManager = vi.mocked(DefaultOdpSegmentManager); + const MockDefaultOdpSegmentApiManager = vi.mocked(DefaultOdpSegmentApiManager); + const MockInMemoryLruCache = vi.mocked(InMemoryLruCache); + const MockDefaultOdpEventManager = vi.mocked(DefaultOdpEventManager); + const MockDefaultOdpEventApiManager = vi.mocked(DefaultOdpEventApiManager); + const MockIntervalRepeater = vi.mocked(IntervalRepeater); + const MockExponentialBackoff = vi.mocked(ExponentialBackoff); + + beforeEach(() => { + MockDefaultOdpManager.mockClear(); + MockDefaultOdpSegmentManager.mockClear(); + MockDefaultOdpSegmentApiManager.mockClear(); + MockInMemoryLruCache.mockClear(); + MockDefaultOdpEventManager.mockClear(); + MockDefaultOdpEventApiManager.mockClear(); + MockIntervalRepeater.mockClear(); + MockExponentialBackoff.mockClear(); + }); + + it('should throw and error if provided segment cache is invalid', () => { + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: 'abc' as any + })).toThrow('Invalid cache'); + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: {} as any, + })).toThrow('Invalid cache method save, Invalid cache method lookup, Invalid cache method reset'); + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: { save: 'abc', lookup: 'abc', reset: 'abc' } as any, + })).toThrow('Invalid cache method save, Invalid cache method lookup, Invalid cache method reset'); + + const noop = () => {}; + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: { save: noop, lookup: 'abc', reset: 'abc' } as any, + })).toThrow('Invalid cache method lookup, Invalid cache method reset'); + + expect(() => getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCache: { save: noop, lookup: noop, reset: 'abc' } as any, + })).toThrow('Invalid cache method reset'); + }); + + describe('segment manager', () => { + it('should create a default segment manager with default api manager using the passed eventRequestHandler', () => { + const segmentRequestHandler = getMockRequestHandler(); + const odpManager = getOdpManager({ + segmentRequestHandler, + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const apiManager = MockDefaultOdpSegmentManager.mock.calls[0][1]; + expect(Object.is(apiManager, MockDefaultOdpSegmentApiManager.mock.instances[0])).toBe(true); + const usedRequestHandler = MockDefaultOdpSegmentApiManager.mock.calls[0][0]; + expect(Object.is(usedRequestHandler, segmentRequestHandler)).toBe(true); + }); + + it('should create a default segment manager with the provided segment cache', () => { + const segmentsCache = getMockSyncCache<string[]>(); + + const odpManager = getOdpManager({ + segmentsCache, + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(segmentsCache); + }); + + describe('when no segment cache is provided', () => { + it('should use a InMemoryLruCache with the provided size', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCacheSize: 3141, + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][0]).toBe(3141); + }); + + it('should use a InMemoryLruCache with default size if no segmentCacheSize is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][0]).toBe(DEFAULT_CACHE_SIZE); + }); + + it('should use a InMemoryLruCache with the provided timeout', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCacheTimeout: 123456, + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][1]).toBe(123456); + }); + + it('should use a InMemoryLruCache with default timeout if no segmentsCacheTimeout is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][1]).toBe(DEFAULT_CACHE_TIMEOUT); + }); + }); + }); + + describe('event manager', () => { + it('should use a default event manager with default api manager using the passed eventRequestHandler and eventRequestGenerator', () => { + const eventRequestHandler = getMockRequestHandler(); + const eventRequestGenerator = vi.fn(); + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler, + eventRequestGenerator, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const apiManager = MockDefaultOdpEventManager.mock.calls[0][0].apiManager; + expect(apiManager).toBe(MockDefaultOdpEventApiManager.mock.instances[0]); + const usedRequestHandler = MockDefaultOdpEventApiManager.mock.calls[0][0]; + expect(usedRequestHandler).toBe(eventRequestHandler); + const usedRequestGenerator = MockDefaultOdpEventApiManager.mock.calls[0][1]; + expect(usedRequestGenerator).toBe(eventRequestGenerator); + }); + + it('should use a default event manager with the provided event batch size', () => { + const eventBatchSize = 1234; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventBatchSize, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBatchSize = MockDefaultOdpEventManager.mock.calls[0][0].batchSize; + expect(usedBatchSize).toBe(eventBatchSize); + }); + + it('should use a default event manager with the default batch size if no eventBatchSize is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBatchSize = MockDefaultOdpEventManager.mock.calls[0][0].batchSize; + expect(usedBatchSize).toBe(DEFAULT_EVENT_BATCH_SIZE); + }); + + it('should use a default event manager with an interval repeater with the provided flush interval', () => { + const eventFlushInterval = 1234; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventFlushInterval, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedRepeater = MockDefaultOdpEventManager.mock.calls[0][0].repeater; + expect(usedRepeater).toBe(MockIntervalRepeater.mock.instances[0]); + const usedInterval = MockIntervalRepeater.mock.calls[0][0]; + expect(usedInterval).toBe(eventFlushInterval); + }); + + it('should use a default event manager with the provided max retries', () => { + const eventMaxRetries = 7; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventMaxRetries, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedMaxRetries = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.maxRetries; + expect(usedMaxRetries).toBe(eventMaxRetries); + }); + + it('should use a default event manager with the default max retries if no eventMaxRetries is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedMaxRetries = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.maxRetries; + expect(usedMaxRetries).toBe(DEFAULT_EVENT_MAX_RETRIES); + }); + + it('should use a default event manager with ExponentialBackoff with provided minBackoff', () => { + const eventMinBackoff = 1234; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventMinBackoff, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][0]).toBe(eventMinBackoff); + }); + + it('should use a default event manager with ExponentialBackoff with default min backoff if none provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][0]).toBe(DEFAULT_EVENT_MIN_BACKOFF); + }); + + it('should use a default event manager with ExponentialBackoff with provided maxBackoff', () => { + const eventMaxBackoff = 9999; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventMaxBackoff: eventMaxBackoff, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][1]).toBe(eventMaxBackoff); + }); + + it('should use a default event manager with ExponentialBackoff with default max backoff if none provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][1]).toBe(DEFAULT_EVENT_MAX_BACKOFF); + }); + }); + + it('should use the provided userAgentParser', () => { + const userAgentParser = {} as any; + + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + userAgentParser, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { userAgentParser: usedUserAgentParser } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedUserAgentParser).toBe(userAgentParser); + }); +}); diff --git a/lib/odp/odp_manager_factory.ts b/lib/odp/odp_manager_factory.ts new file mode 100644 index 000000000..9fd689964 --- /dev/null +++ b/lib/odp/odp_manager_factory.ts @@ -0,0 +1,138 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestHandler } from "../shared_types"; +import { Cache } from "../utils/cache/cache"; +import { InMemoryLruCache } from "../utils/cache/in_memory_lru_cache"; +import { ExponentialBackoff, IntervalRepeater } from "../utils/repeater/repeater"; +import { Maybe } from "../utils/type"; +import { DefaultOdpEventApiManager, EventRequestGenerator } from "./event_manager/odp_event_api_manager"; +import { DefaultOdpEventManager, OdpEventManager } from "./event_manager/odp_event_manager"; +import { DefaultOdpManager, OdpManager } from "./odp_manager"; +import { DefaultOdpSegmentApiManager } from "./segment_manager/odp_segment_api_manager"; +import { DefaultOdpSegmentManager, OdpSegmentManager } from "./segment_manager/odp_segment_manager"; +import { UserAgentParser } from "./ua_parser/user_agent_parser"; + +export const DEFAULT_CACHE_SIZE = 1000; +export const DEFAULT_CACHE_TIMEOUT = 600_000; + +export const DEFAULT_EVENT_BATCH_SIZE = 100; +export const DEFAULT_EVENT_FLUSH_INTERVAL = 10_000; +export const DEFAULT_EVENT_MAX_RETRIES = 5; +export const DEFAULT_EVENT_MIN_BACKOFF = 1000; +export const DEFAULT_EVENT_MAX_BACKOFF = 32_000; + +export const INVALID_CACHE = 'Invalid cache'; +export const INVALID_CACHE_METHOD = 'Invalid cache method %s'; + +const odpManagerSymbol: unique symbol = Symbol(); + +export type OpaqueOdpManager = { + [odpManagerSymbol]: unknown; +}; + +export type OdpManagerOptions = { + segmentsCache?: Cache<string[]>; + segmentsCacheSize?: number; + segmentsCacheTimeout?: number; + segmentsApiTimeout?: number; + eventFlushInterval?: number; + eventBatchSize?: number; + eventApiTimeout?: number; + userAgentParser?: UserAgentParser; +}; + +export type OdpManagerFactoryOptions = Omit<OdpManagerOptions, 'segmentsApiTimeout' | 'eventApiTimeout'> & { + segmentRequestHandler: RequestHandler; + eventRequestHandler: RequestHandler; + eventRequestGenerator: EventRequestGenerator; + eventMaxRetries?: number; + eventMinBackoff?: number; + eventMaxBackoff?: number; +} + +const validateCache = (cache: any) => { + const errors = []; + if (!cache || typeof cache !== 'object') { + throw new Error(INVALID_CACHE); + } + + for (const method of ['save', 'lookup', 'reset']) { + if (typeof cache[method] !== 'function') { + errors.push(INVALID_CACHE_METHOD.replace('%s', method)); + } + } + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } +} + +const getDefaultSegmentsCache = (cacheSize?: number, cacheTimeout?: number) => { + return new InMemoryLruCache<string[]>(cacheSize || DEFAULT_CACHE_SIZE, cacheTimeout || DEFAULT_CACHE_TIMEOUT); +} + +const getDefaultSegmentManager = (options: OdpManagerFactoryOptions) => { + if (options.segmentsCache) { + validateCache(options.segmentsCache); + } + + return new DefaultOdpSegmentManager( + options.segmentsCache || getDefaultSegmentsCache(options.segmentsCacheSize, options.segmentsCacheTimeout), + new DefaultOdpSegmentApiManager(options.segmentRequestHandler), + ); +}; + +const getDefaultEventManager = (options: OdpManagerFactoryOptions) => { + return new DefaultOdpEventManager({ + apiManager: new DefaultOdpEventApiManager(options.eventRequestHandler, options.eventRequestGenerator), + batchSize: options.eventBatchSize || DEFAULT_EVENT_BATCH_SIZE, + repeater: new IntervalRepeater(options.eventFlushInterval || DEFAULT_EVENT_FLUSH_INTERVAL), + retryConfig: { + maxRetries: options.eventMaxRetries || DEFAULT_EVENT_MAX_RETRIES, + backoffProvider: () => new ExponentialBackoff( + options.eventMinBackoff || DEFAULT_EVENT_MIN_BACKOFF, + options.eventMaxBackoff || DEFAULT_EVENT_MAX_BACKOFF, + 500, + ), + }, + }); +} + +export const getOdpManager = (options: OdpManagerFactoryOptions): OdpManager => { + const segmentManager = getDefaultSegmentManager(options); + const eventManager = getDefaultEventManager(options); + + return new DefaultOdpManager({ + segmentManager, + eventManager, + userAgentParser: options.userAgentParser, + }); +}; + +export const getOpaqueOdpManager = (options: OdpManagerFactoryOptions): OpaqueOdpManager => { + return { + [odpManagerSymbol]: getOdpManager(options), + }; +}; + +export const extractOdpManager = (manager: Maybe<OpaqueOdpManager>): Maybe<OdpManager> => { + if (!manager || typeof manager !== 'object') { + return undefined; + } + + return manager[odpManagerSymbol] as Maybe<OdpManager>; +} diff --git a/lib/odp/odp_manager_factory.universal.ts b/lib/odp/odp_manager_factory.universal.ts new file mode 100644 index 000000000..6bf509611 --- /dev/null +++ b/lib/odp/odp_manager_factory.universal.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestHandler } from '../utils/http_request_handler/http'; +import { validateRequestHandler } from '../utils/http_request_handler/request_handler_validator'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + +export const DEFAULT_API_TIMEOUT = 10_000; +export const DEFAULT_BATCH_SIZE = 1; +export const DEFAULT_FLUSH_INTERVAL = 1000; + +export type UniversalOdpManagerOptions = OdpManagerOptions & { + requestHandler: RequestHandler; +}; + +export const createOdpManager = (options: UniversalOdpManagerOptions): OpaqueOdpManager => { + validateRequestHandler(options.requestHandler); + return getOpaqueOdpManager({ + ...options, + segmentRequestHandler: options.requestHandler, + eventRequestHandler: options.requestHandler, + eventBatchSize: options.eventBatchSize || DEFAULT_BATCH_SIZE, + eventFlushInterval: options.eventFlushInterval || DEFAULT_FLUSH_INTERVAL, + eventRequestGenerator: eventApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_types.ts b/lib/odp/odp_types.ts new file mode 100644 index 000000000..abe47b245 --- /dev/null +++ b/lib/odp/odp_types.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Wrapper around valid data and error responses + */ +export interface Response { + data: Data; + errors: Error[]; +} + +/** + * GraphQL response data returned from a valid query + */ +export interface Data { + customer: Customer; +} + +/** + * GraphQL response from an errant query + */ +export interface Error { + message: string; + locations: Location[]; + path: string[]; + extensions: Extension; +} + +/** + * Profile used to group/segment an addressable market + */ +export interface Customer { + audiences: Audience; +} + +/** + * Specifies the precise place in code or data where the error occurred + */ +export interface Location { + line: number; + column: number; +} + +/** + * Extended error information + */ +export interface Extension { + code: string; + classification: string; +} + +/** + * Segment of a customer base + */ +export interface Audience { + edges: Edge[]; +} + +/** + * Grouping of nodes within an audience + */ +export interface Edge { + node: Node; +} + +/** + * Atomic grouping an audience + */ +export interface Node { + name: string; + state: string; +} diff --git a/lib/odp/segment_manager/odp_response_schema.ts b/lib/odp/segment_manager/odp_response_schema.ts new file mode 100644 index 000000000..4221178af --- /dev/null +++ b/lib/odp/segment_manager/odp_response_schema.ts @@ -0,0 +1,186 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSONSchema4 } from 'json-schema'; + +/** + * JSON Schema used to validate the ODP GraphQL response + */ +export const OdpResponseSchema = { + $schema: 'https://json-schema.org/draft/2019-09/schema', + $id: 'https://example.com/example.json', + title: 'ODP Response Schema', + type: 'object', + required: [ + 'data', + ], + properties: { + data: { + title: 'The data Schema', + type: 'object', + required: [ + 'customer', + ], + properties: { + customer: { + title: 'The customer Schema', + type: 'object', + required: [], + properties: { + audiences: { + title: 'The audiences Schema', + type: 'object', + required: [ + 'edges', + ], + properties: { + edges: { + title: 'The edges Schema', + type: 'array', + items: { + title: 'A Schema', + type: 'object', + required: [ + 'node', + ], + properties: { + node: { + title: 'The node Schema', + type: 'object', + required: [ + 'name', + 'state', + ], + properties: { + name: { + title: 'The name Schema', + type: 'string', + examples: [ + 'has_email', + 'has_email_opted_in', + ], + }, + state: { + title: 'The state Schema', + type: 'string', + examples: [ + 'qualified', + ], + }, + }, + examples: [], + }, + }, + examples: [], + }, + examples: [], + }, + }, + examples: [], + }, + }, + examples: [], + }, + }, + examples: [], + }, + errors: { + title: 'The errors Schema', + type: 'array', + default: [], + items: { + title: 'A Schema', + type: 'object', + required: [ + 'message', + 'locations', + 'extensions', + ], + properties: { + message: { + title: 'The message Schema', + type: 'string', + examples: [ + 'Exception while fetching data (/customer) : java.lang.RuntimeException: could not resolve _fs_user_id = asdsdaddddd', + ], + }, + locations: { + title: 'The locations Schema', + type: 'array', + items: { + title: 'A Schema', + type: 'object', + required: [ + 'line', + 'column', + ], + properties: { + line: { + title: 'The line Schema', + type: 'integer', + examples: [ + 2, + ], + }, + column: { + title: 'The column Schema', + type: 'integer', + examples: [ + 3, + ], + }, + }, + examples: [], + }, + examples: [], + }, + path: { + title: 'The path Schema', + type: 'array', + items: { + title: 'A Schema', + type: 'string', + examples: [ + 'customer', + ], + }, + examples: [], + }, + extensions: { + title: 'The extensions Schema', + type: 'object', + required: [ + 'classification', + ], + properties: { + classification: { + title: 'The classification Schema', + type: 'string', + examples: [ + 'InvalidIdentifierException', + ], + }, + }, + examples: [], + }, + }, + examples: [], + }, + examples: [], + }, + }, + examples: [], +} as JSONSchema4; diff --git a/lib/odp/segment_manager/odp_segment_api_manager.spec.ts b/lib/odp/segment_manager/odp_segment_api_manager.spec.ts new file mode 100644 index 000000000..7906f5745 --- /dev/null +++ b/lib/odp/segment_manager/odp_segment_api_manager.spec.ts @@ -0,0 +1,262 @@ +/** + * Copyright 2022-2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; + +import { ODP_USER_KEY } from '../constant'; +import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { DefaultOdpSegmentApiManager, LOGGER_NAME } from './odp_segment_api_manager'; + +const API_KEY = 'not-real-api-key'; +const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; +const USER_KEY = ODP_USER_KEY.FS_USER_ID; +const USER_VALUE = 'tester-101'; +const SEGMENTS_TO_CHECK = ['has_email', 'has_email_opted_in', 'push_on_sale']; + +describe('DefaultOdpSegmentApiManager', () => { + it('should set name on the logger passed into the constructor', () => { + const logger = getMockLogger(); + + const manager = new DefaultOdpSegmentApiManager(getMockRequestHandler(), logger); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should set name on the logger set by setLogger', () => { + const logger = getMockLogger(); + + const manager = new DefaultOdpSegmentApiManager(getMockRequestHandler()); + manager.setLogger(logger); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should return empty list without calling api when segmentsToCheck is empty', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: '' }), + }); + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, []); + + expect(segments).toEqual([]); + expect(requestHandler.makeRequest).not.toHaveBeenCalled(); + }); + + it('should return null and log error if requestHandler promise rejects', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.reject(new Error("Request timed out")), + }); + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should log error and return null in case of non 200 HTTP status code response', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 500, body: '' }), + }); + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should return null and log error if response body is invalid JSON', async () => { + const invalidJsonResponse = 'not-a-valid-json-response'; + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: invalidJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should return null and log error if response body is unrecognized JSON', async () => { + const invalidJsonResponse = '{"a":1}'; + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: invalidJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should log error and return null in case of invalid identifier error response', async () => { + const INVALID_USER_ID = 'invalid-user'; + const errorJsonResponse = + '{"errors":[{"message":' + + '"Exception while fetching data (/customer) : ' + + `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + + '"locations":[{"line":1,"column":8}],"path":["customer"],' + + '"extensions":{"code": "INVALID_IDENTIFIER_EXCEPTION","classification":"DataFetchingException"}}],' + + '"data":{"customer":null}}'; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: errorJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, 'mock_user_id', SEGMENTS_TO_CHECK); + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledWith('Audience segments fetch failed (invalid identifier)'); + }); + + it('should log error and return null in case of errors other than invalid identifier error', async () => { + const INVALID_USER_ID = 'invalid-user'; + const errorJsonResponse = + '{"errors":[{"message":' + + '"Exception while fetching data (/customer) : ' + + `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + + '"locations":[{"line":1,"column":8}],"path":["customer"],' + + '"extensions":{"classification":"DataFetchingException"}}],' + + '"data":{"customer":null}}'; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: errorJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, 'mock_user_id', SEGMENTS_TO_CHECK); + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledWith('Audience segments fetch failed (DataFetchingException)'); + }); + + it('should log error and return null in case of response with invalid falsy edges field', async () => { + const jsonResponse = `{ + "data": { + "customer": { + "audiences": { + } + } + } + }`; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: jsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should parse a success response and return qualified segments', async () => { + const validJsonResponse = `{ + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "has_email", + "state": "qualified" + } + }, + { + "node": { + "name": "has_email_opted_in", + "state": "not-qualified" + } + } + ] + } + } + } + }`; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: validJsonResponse }), + }); + + const manager = new DefaultOdpSegmentApiManager(requestHandler); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toEqual(['has_email']); + }); + + it('should handle empty qualified segments', async () => { + const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: responseJsonWithNoQualifiedSegments }), + }); + + const manager = new DefaultOdpSegmentApiManager(requestHandler); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toEqual([]); + }); + + it('should construct a valid GraphQL query request', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: '' }), + }); + + const manager = new DefaultOdpSegmentApiManager(requestHandler); + await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(requestHandler.makeRequest).toHaveBeenCalledWith( + `${GRAPHQL_ENDPOINT}/v3/graphql`, + { + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + }, + 'POST', + `{"query" : "query {customer(${USER_KEY} : \\"${USER_VALUE}\\") {audiences(subset: [\\"has_email\\",\\"has_email_opted_in\\",\\"push_on_sale\\"]) {edges {node {name state}}}}}"}` + ); + }); +}); diff --git a/lib/odp/segment_manager/odp_segment_api_manager.ts b/lib/odp/segment_manager/odp_segment_api_manager.ts new file mode 100644 index 000000000..92eeaa02e --- /dev/null +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -0,0 +1,198 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerFacade } from '../../logging/logger'; +import { validate } from '../../utils/json_schema_validator'; +import { OdpResponseSchema } from './odp_response_schema'; +import { ODP_USER_KEY } from '../constant'; +import { RequestHandler } from '../../utils/http_request_handler/http'; +import { Response as GraphQLResponse } from '../odp_types'; +import { log } from 'console'; +/** + * Expected value for a qualified/valid segment + */ +const QUALIFIED = 'qualified'; +/** + * Return value when no valid segments found + */ +const EMPTY_SEGMENTS_COLLECTION: string[] = []; +/** + * Return value for scenarios with no valid JSON + */ +const EMPTY_JSON_RESPONSE = null; +/** + * Standard message for audience querying fetch errors + */ +const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; + +/** + * Manager for communicating with the Optimizely Data Platform GraphQL endpoint + */ +export interface OdpSegmentApiManager { + fetchSegments( + apiKey: string, + apiHost: string, + userKey: string, + userValue: string, + segmentsToCheck: string[] + ): Promise<string[] | null>; + setLogger(logger: LoggerFacade): void; +} + +export const LOGGER_NAME = 'OdpSegmentApiManager'; + +export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { + private logger?: LoggerFacade; + private requestHandler: RequestHandler; + + constructor(requestHandler: RequestHandler, logger?: LoggerFacade) { + this.requestHandler = requestHandler; + if (logger) { + this.setLogger(logger); + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + } + + /** + * Retrieves the audience segments from ODP + * @param apiKey ODP public key + * @param apiHost Host of ODP endpoint + * @param userKey 'vuid' or 'fs_user_id key' + * @param userValue Associated value to query for the user key + * @param segmentsToCheck Audience segments to check for experiment inclusion + */ + async fetchSegments( + apiKey: string, + apiHost: string, + userKey: ODP_USER_KEY, + userValue: string, + segmentsToCheck: string[] + ): Promise<string[] | null> { + if (segmentsToCheck?.length === 0) { + return EMPTY_SEGMENTS_COLLECTION; + } + + const endpoint = `${apiHost}/v3/graphql`; + const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); + + const segmentsResponse = await this.querySegments(apiKey, endpoint, query); + if (!segmentsResponse) { + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); + return null; + } + + const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); + if (!parsedSegments) { + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); + return null; + } + + if (parsedSegments.errors?.length > 0) { + const { code, classification } = parsedSegments.errors[0].extensions; + + if (code == 'INVALID_IDENTIFIER_EXCEPTION') { + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (invalid identifier)`); + } else { + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (${classification})`); + } + + return null; + } + + const edges = parsedSegments?.data?.customer?.audiences?.edges; + if (!edges) { + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); + return null; + } + + return edges.filter(edge => edge.node.state == QUALIFIED).map(edge => edge.node.name); + } + + /** + * Converts the query parameters to a GraphQL JSON payload + * @returns GraphQL JSON string + */ + private toGraphQLJson = (userKey: string, userValue: string, segmentsToCheck: string[]): string => + [ + '{"query" : "query {customer', + `(${userKey} : \\"${userValue}\\") `, + '{audiences', + '(subset: [', + ...(segmentsToCheck?.map( + (segment, index) => `\\"${segment}\\"${index < segmentsToCheck.length - 1 ? ',' : ''}` + ) || ''), + ']) {edges {node {name state}}}}}"}', + ].join(''); + + /** + * Handler for querying the ODP GraphQL endpoint + * @param apiKey ODP API key + * @param endpoint Fully-qualified GraphQL endpoint URL + * @param userKey 'vuid' or 'fs_user_id' + * @param userValue userKey's value + * @param query GraphQL formatted query string + * @returns JSON response string from ODP or null + */ + private async querySegments( + apiKey: string, + endpoint: string, + query: string + ): Promise<string | null> { + const method = 'POST'; + const url = endpoint; + const headers = { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }; + + try { + const request = this.requestHandler.makeRequest(url, headers, method, query); + const { statusCode, body} = await request.responsePromise; + if (!(statusCode >= 200 && statusCode < 300)) { + return null; + } + return body; + } catch { + return null; + } + } + + /** + * Parses JSON response + * @param jsonResponse JSON response from ODP + * @private + * @returns Response Strongly-typed ODP Response object + */ + private parseSegmentsResponseJson(jsonResponse: string): GraphQLResponse | null { + let jsonObject = {}; + + try { + jsonObject = JSON.parse(jsonResponse); + } catch { + return EMPTY_JSON_RESPONSE; + } + + if (validate(jsonObject, OdpResponseSchema, false)) { + return jsonObject as GraphQLResponse; + } + + return EMPTY_JSON_RESPONSE; + } +} diff --git a/lib/odp/segment_manager/odp_segment_manager.spec.ts b/lib/odp/segment_manager/odp_segment_manager.spec.ts new file mode 100644 index 000000000..550e431b5 --- /dev/null +++ b/lib/odp/segment_manager/odp_segment_manager.spec.ts @@ -0,0 +1,226 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi } from 'vitest'; + + +import { ODP_USER_KEY } from '../constant'; +import { DefaultOdpSegmentManager } from './odp_segment_manager'; +import { OdpConfig } from '../odp_config'; +import { OptimizelySegmentOption } from './optimizely_segment_option'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { getMockSyncCache } from '../../tests/mock/mock_cache'; +import { LOGGER_NAME } from './odp_segment_manager'; + +const API_KEY = 'test-api-key'; +const API_HOST = 'https://odp.example.com'; +const PIXEL_URL = 'https://odp.pixel.com'; +const SEGMENTS_TO_CHECK = ['segment1', 'segment2']; + +const config = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, SEGMENTS_TO_CHECK); + +const getMockApiManager = () => { + return { + fetchSegments: vi.fn(), + setLogger: vi.fn(), + }; +}; + +const userKey: ODP_USER_KEY = ODP_USER_KEY.FS_USER_ID; +const userValue = 'test-user'; + +describe('DefaultOdpSegmentManager', () => { + describe('a logger is passed in the constructor', () => { + it('should set name on the logger passed into the constructor', () => { + const logger = getMockLogger(); + const cache = getMockSyncCache<string[]>(); + const manager = new DefaultOdpSegmentManager(cache, getMockApiManager(), logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should pass a child logger to the segmentApiManager', () => { + const logger = getMockLogger(); + const childLogger = getMockLogger(); + logger.child.mockReturnValue(childLogger); + + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + const manager = new DefaultOdpSegmentManager(cache, apiManager, logger); + + expect(apiManager.setLogger).toHaveBeenCalledWith(childLogger); + }); + }); + + describe('setLogger method', () => { + it('should set name on the logger', () => { + const logger = getMockLogger(); + const cache = getMockSyncCache<string[]>(); + const manager = new DefaultOdpSegmentManager(cache, getMockApiManager()); + manager.setLogger(logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME); + }); + + it('should pass a child logger to the datafileManager', () => { + const logger = getMockLogger(); + const childLogger = getMockLogger(); + logger.child.mockReturnValue(childLogger); + + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.setLogger(logger); + + expect(apiManager.setLogger).toHaveBeenCalledWith(childLogger); + }); + }); + + it('should return null and log error if the ODP config is not available.', async () => { + const logger = getMockLogger(); + const cache = getMockSyncCache<string[]>(); + const manager = new DefaultOdpSegmentManager(cache, getMockApiManager(), logger); + expect(await manager.fetchQualifiedSegments(userKey, userValue)).toBeNull(); + expect(logger.warn).toHaveBeenCalledOnce(); + }); + + it('should return null and log error if ODP is not integrated.', async () => { + const logger = getMockLogger(); + const cache = getMockSyncCache<string[]>(); + const manager = new DefaultOdpSegmentManager(cache, getMockApiManager(), logger); + manager.updateConfig({ integrated: false }); + expect(await manager.fetchQualifiedSegments(userKey, userValue)).toBeNull(); + expect(logger.warn).toHaveBeenCalledOnce(); + }); + + it('should fetch segments from apiManager using correct config on cache miss and save to cache.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue); + expect(segments).toEqual(['k', 'l']); + expect(apiManager.fetchSegments).toHaveBeenCalledWith(API_KEY, API_HOST, userKey, userValue, SEGMENTS_TO_CHECK); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); + }); + + it('should return segment from cache and not call apiManager on cache hit.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue); + expect(segments).toEqual(['x']); + + expect(apiManager.fetchSegments).not.toHaveBeenCalled(); + }); + + it('should return null when apiManager returns null.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(null); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue); + expect(segments).toBeNull(); + }); + + it('should ignore the cache if the option enum is included in the options array.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue, [OptimizelySegmentOption.IGNORE_CACHE]); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['x']); + expect(apiManager.fetchSegments).toHaveBeenCalledWith(API_KEY, API_HOST, userKey, userValue, SEGMENTS_TO_CHECK); + }); + + it('should ignore the cache if the option string is included in the options array.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const segments = await manager.fetchQualifiedSegments(userKey, userValue, ['IGNORE_CACHE']); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['x']); + expect(apiManager.fetchSegments).toHaveBeenCalledWith(API_KEY, API_HOST, userKey, userValue, SEGMENTS_TO_CHECK); + }); + + it('should reset the cache if the option enum is included in the options array.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + cache.set(manager.makeCacheKey(userKey, '123'), ['a']); + cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue, [OptimizelySegmentOption.RESET_CACHE]); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); + expect(cache.size()).toBe(1); + }); + + it('should reset the cache if the option string is included in the options array.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + cache.set(manager.makeCacheKey(userKey, '123'), ['a']); + cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const segments = await manager.fetchQualifiedSegments(userKey, userValue, ['RESET_CACHE']); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); + expect(cache.size()).toBe(1); + }); + + it('should reset the cache on config update.', async () => { + const cache = getMockSyncCache<string[]>(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + cache.set(manager.makeCacheKey(userKey, '123'), ['a']); + cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + + expect(cache.size()).toBe(3); + manager.updateConfig({ integrated: true, odpConfig: config }); + expect(cache.size()).toBe(0); + }); +}); diff --git a/lib/odp/segment_manager/odp_segment_manager.ts b/lib/odp/segment_manager/odp_segment_manager.ts new file mode 100644 index 000000000..4ff125672 --- /dev/null +++ b/lib/odp/segment_manager/odp_segment_manager.ts @@ -0,0 +1,130 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Cache } from '../../utils/cache/cache'; +import { OdpSegmentApiManager } from './odp_segment_api_manager'; +import { OdpIntegrationConfig } from '../odp_config'; +import { OptimizelySegmentOption } from './optimizely_segment_option'; +import { ODP_USER_KEY } from '../constant'; +import { LoggerFacade } from '../../logging/logger'; +import { ODP_CONFIG_NOT_AVAILABLE, ODP_NOT_INTEGRATED } from 'error_message'; + +export interface OdpSegmentManager { + fetchQualifiedSegments( + userKey: ODP_USER_KEY, + userValue: string, + options?: Array<OptimizelySegmentOption> + ): Promise<string[] | null>; + updateConfig(config: OdpIntegrationConfig): void; + setLogger(logger: LoggerFacade): void; +} + +export const LOGGER_NAME = 'OdpSegmentManager'; + +export class DefaultOdpSegmentManager implements OdpSegmentManager { + private odpIntegrationConfig?: OdpIntegrationConfig; + private segmentsCache: Cache<string[]>; + private odpSegmentApiManager: OdpSegmentApiManager + private logger?: LoggerFacade; + + constructor( + segmentsCache: Cache<string[]>, + odpSegmentApiManager: OdpSegmentApiManager, + logger?: LoggerFacade, + ) { + this.segmentsCache = segmentsCache; + this.odpSegmentApiManager = odpSegmentApiManager; + if (logger) { + this.setLogger(logger); + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + this.odpSegmentApiManager.setLogger(logger.child()); + } + + /** + * Attempts to fetch and return a list of a user's qualified segments from the local segments cache. + * If no cached data exists for the target user, this fetches and caches data from the ODP server instead. + * @param userKey Key used for identifying the id type. + * @param userValue The id value itself. + * @param options An array of OptimizelySegmentOption used to ignore and/or reset the cache. + * @returns Qualified segments for the user from the cache or the ODP server if the cache is empty. + */ + async fetchQualifiedSegments( + userKey: ODP_USER_KEY, + userValue: string, + options?: Array<OptimizelySegmentOption> + ): Promise<string[] | null> { + if (!this.odpIntegrationConfig) { + this.logger?.warn(ODP_CONFIG_NOT_AVAILABLE); + return null; + } + + if (!this.odpIntegrationConfig.integrated) { + this.logger?.warn(ODP_NOT_INTEGRATED); + return null; + } + + const odpConfig = this.odpIntegrationConfig.odpConfig; + + const segmentsToCheck = odpConfig.segmentsToCheck; + if (!segmentsToCheck || segmentsToCheck.length <= 0) { + return []; + } + + const cacheKey = this.makeCacheKey(userKey, userValue); + + const ignoreCache = options?.includes(OptimizelySegmentOption.IGNORE_CACHE); + const resetCache = options?.includes(OptimizelySegmentOption.RESET_CACHE); + + if (resetCache) { + this.segmentsCache.reset(); + } + + if (!ignoreCache) { + const cachedSegments = await this.segmentsCache.lookup(cacheKey); + if (cachedSegments) { + return cachedSegments; + } + } + + const segments = await this.odpSegmentApiManager.fetchSegments( + odpConfig.apiKey, + odpConfig.apiHost, + userKey, + userValue, + segmentsToCheck + ); + + if (segments && !ignoreCache) { + this.segmentsCache.save(cacheKey, segments); + } + + return segments; + } + + makeCacheKey(userKey: string, userValue: string): string { + return `${userKey}-$-${userValue}`; + } + + updateConfig(config: OdpIntegrationConfig): void { + this.odpIntegrationConfig = config; + this.segmentsCache.reset(); + } +} diff --git a/lib/odp/segment_manager/optimizely_segment_option.ts b/lib/odp/segment_manager/optimizely_segment_option.ts new file mode 100644 index 000000000..cf7c801ef --- /dev/null +++ b/lib/odp/segment_manager/optimizely_segment_option.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Options for defining behavior of OdpSegmentManager's caching mechanism when calling fetchSegments() +export enum OptimizelySegmentOption { + IGNORE_CACHE = 'IGNORE_CACHE', + RESET_CACHE = 'RESET_CACHE', +} diff --git a/lib/odp/ua_parser/ua_parser.ts b/lib/odp/ua_parser/ua_parser.ts new file mode 100644 index 000000000..8622b0ade --- /dev/null +++ b/lib/odp/ua_parser/ua_parser.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2023, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UAParser } from 'ua-parser-js'; +import { UserAgentInfo } from './user_agent_info'; +import { UserAgentParser } from './user_agent_parser'; + +const userAgentParser: UserAgentParser = { + parseUserAgentInfo(): UserAgentInfo { + const parser = new UAParser(); + const agentInfo = parser.getResult(); + const { os, device } = agentInfo; + return { os, device }; + } +} + +export function getUserAgentParser(): UserAgentParser { + return userAgentParser; +} diff --git a/packages/optimizely-sdk/lib/plugins/logger/enums.js b/lib/odp/ua_parser/user_agent_info.ts similarity index 70% rename from packages/optimizely-sdk/lib/plugins/logger/enums.js rename to lib/odp/ua_parser/user_agent_info.ts index 2d22c57b8..e83b3b032 100644 --- a/packages/optimizely-sdk/lib/plugins/logger/enums.js +++ b/lib/odp/ua_parser/user_agent_info.ts @@ -1,11 +1,11 @@ /** - * Copyright 2016, Optimizely + * Copyright 2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,4 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -exports.LOG_LEVEL = require('@optimizely/js-sdk-logging').LogLevel; \ No newline at end of file + +export type UserAgentInfo = { + os: { + name?: string, + version?: string, + }, + device: { + type?: string, + model?: string, + } +}; diff --git a/packages/logging/src/index.ts b/lib/odp/ua_parser/user_agent_parser.ts similarity index 71% rename from packages/logging/src/index.ts rename to lib/odp/ua_parser/user_agent_parser.ts index 47a1e99c8..9ca30c141 100644 --- a/packages/logging/src/index.ts +++ b/lib/odp/ua_parser/user_agent_parser.ts @@ -1,11 +1,11 @@ /** - * Copyright 2019, Optimizely + * Copyright 2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './errorHandler' -export * from './models' -export * from './logger' + +import { UserAgentInfo } from "./user_agent_info"; + +export interface UserAgentParser { + parseUserAgentInfo(): UserAgentInfo, +} diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts new file mode 100644 index 000000000..4548ffbb7 --- /dev/null +++ b/lib/optimizely/index.spec.ts @@ -0,0 +1,908 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import Optimizely from '.'; +import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; +import * as jsonSchemaValidator from '../utils/json_schema_validator'; +import testData from '../tests/test_data'; +import { getForwardingEventProcessor } from '../event_processor/event_processor_factory'; +import { createProjectConfig } from '../project_config/project_config'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { createOdpManager } from '../odp/odp_manager_factory.node'; +import { extractOdpManager } from '../odp/odp_manager_factory'; +import { Value } from '../utils/promise/operation_value'; +import { getDecisionTestDatafile } from '../tests/decision_test_datafile'; +import { DECISION_SOURCES } from '../utils/enums'; +import OptimizelyUserContext from '../optimizely_user_context'; +import { newErrorDecision } from '../optimizely_decision'; +import { ImpressionEvent } from '../event_processor/event_builder/user_event'; +import { OptimizelyDecideOption } from '../shared_types'; +import { NOTIFICATION_TYPES, DECISION_NOTIFICATION_TYPES } from '../notification_center/type'; + + +const holdoutData = [ + { + id: 'holdout_test_id', + key: 'holdout_test_key', + status: 'Running', + includedFlags: [], + excludedFlags: [], + audienceIds: [], + audienceConditions: [], + variations: [ + { + id: 'holdout_variation_id', + key: 'holdout_variation_key', + variables: [], + featureEnabled: false, + }, + ], + trafficAllocation: [ + { + entityId: 'holdout_variation_id', + endOfRange: 10000, + }, + ], + }, +]; + +describe('Optimizely', () => { + const eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; + + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + const odpManager = extractOdpManager(createOdpManager({})); + const logger = getMockLogger(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should pass disposable options to the respective services', () => { + const projectConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + vi.spyOn(projectConfigManager, 'makeDisposable'); + vi.spyOn(eventProcessor, 'makeDisposable'); + vi.spyOn(odpManager!, 'makeDisposable'); + + new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + jsonSchemaValidator, + logger, + eventProcessor, + odpManager, + disposable: true, + cmabService: {} as any + }); + + expect(projectConfigManager.makeDisposable).toHaveBeenCalled(); + expect(eventProcessor.makeDisposable).toHaveBeenCalled(); + expect(odpManager!.makeDisposable).toHaveBeenCalled(); + }); + + it('should set child logger to respective services', () => { + const projectConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + const odpManager = extractOdpManager(createOdpManager({})); + + vi.spyOn(projectConfigManager, 'setLogger'); + vi.spyOn(eventProcessor, 'setLogger'); + vi.spyOn(odpManager!, 'setLogger'); + + const logger = getMockLogger(); + const configChildLogger = getMockLogger(); + const eventProcessorChildLogger = getMockLogger(); + const odpManagerChildLogger = getMockLogger(); + vi.spyOn(logger, 'child').mockReturnValueOnce(configChildLogger) + .mockReturnValueOnce(eventProcessorChildLogger) + .mockReturnValueOnce(odpManagerChildLogger); + + new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + jsonSchemaValidator, + logger, + eventProcessor, + odpManager, + disposable: true, + cmabService: {} as any + }); + + expect(projectConfigManager.setLogger).toHaveBeenCalledWith(configChildLogger); + expect(eventProcessor.setLogger).toHaveBeenCalledWith(eventProcessorChildLogger); + expect(odpManager!.setLogger).toHaveBeenCalledWith(odpManagerChildLogger); + }); + + describe('decideAsync', () => { + it('should return an error decision with correct reasons if decisionService returns error', async () => { + const projectConfig = createProjectConfig(getDecisionTestDatafile()); + + const projectConfigManager = getMockProjectConfigManager({ + initConfig: projectConfig, + }); + + const optimizely = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + jsonSchemaValidator, + logger, + eventProcessor, + odpManager, + disposable: true, + cmabService: {} as any + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const decisionService = optimizely.decisionService; + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: true, + result: { + variation: null, + experiment: projectConfig.experimentKeyMap['exp_3'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons:[ + ['test reason %s', '1'], + ['test reason %s', '2'], + ] + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision).toEqual(newErrorDecision('flag_1', user, ['test reason 1', 'test reason 2'])); + }); + + it('should include cmab uuid in dispatched event if decisionService returns a cmab uuid', async () => { + const projectConfig = createProjectConfig(getDecisionTestDatafile()); + + const projectConfigManager = getMockProjectConfigManager({ + initConfig: projectConfig, + }); + + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + const processSpy = vi.spyOn(eventProcessor, 'process'); + + const optimizely = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator, + logger, + odpManager, + disposable: true, + cmabService: {} as any + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const decisionService = optimizely.decisionService; + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + cmabUuid: 'uuid-cmab', + variation: projectConfig.variationIdMap['5003'], + experiment: projectConfig.experimentKeyMap['exp_3'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.ruleKey).toBe('exp_3'); + expect(decision.flagKey).toBe('flag_1'); + expect(decision.variationKey).toBe('variation_3'); + expect(decision.enabled).toBe(true); + + expect(eventProcessor.process).toHaveBeenCalledOnce(); + const event = processSpy.mock.calls[0][0] as ImpressionEvent; + expect(event.cmabUuid).toBe('uuid-cmab'); + }); + + + }); + + describe('holdout tests', () => { + let projectConfig: any; + let optimizely: any; + let decisionService: any; + let flagNotificationSpy: any; + let activateNotificationSpy: any; + let eventProcessor: any; + + beforeEach(() => { + const datafile = getDecisionTestDatafile(); + datafile.holdouts = JSON.parse(JSON.stringify(holdoutData)); // Deep copy to avoid mutations + projectConfig = createProjectConfig(datafile); + + const projectConfigManager = getMockProjectConfigManager({ + initConfig: projectConfig, + }); + + const mockEventDispatcher = { + dispatchEvent: vi.fn(() => Promise.resolve({ statusCode: 200 })), + }; + eventProcessor = getForwardingEventProcessor(mockEventDispatcher); + + optimizely = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator, + logger, + odpManager, + disposable: true, + cmabService: {} as any + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + decisionService = optimizely.decisionService; + + // Setup notification spy + flagNotificationSpy = vi.fn(); + optimizely.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + flagNotificationSpy + ); + + activateNotificationSpy = vi.fn(); + optimizely.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateNotificationSpy + ); + }); + + it('should dispatch impression event for holdout decision', async () => { + const processSpy = vi.spyOn(eventProcessor, 'process'); + + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: {}, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.ruleKey).toBe('holdout_test_key'); + expect(decision.flagKey).toBe('flag_1'); + expect(decision.variationKey).toBe('holdout_variation_key'); + expect(decision.enabled).toBe(false); + + expect(eventProcessor.process).toHaveBeenCalledOnce(); + + const event = processSpy.mock.calls[0][0] as ImpressionEvent; + + expect(event.type).toBe('impression'); + expect(event.ruleKey).toBe('holdout_test_key'); + expect(event.ruleType).toBe('holdout'); + expect(event.enabled).toBe(false); + }); + + it('should not dispatch impression event for holdout when DISABLE_DECISION_EVENT is used', async () => { + const processSpy = vi.spyOn(eventProcessor, 'process'); + + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: {}, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', [OptimizelyDecideOption.DISABLE_DECISION_EVENT]); + + expect(decision.ruleKey).toBe('holdout_test_key'); + expect(decision.enabled).toBe(false); + expect(processSpy).not.toHaveBeenCalled(); + }); + + it('should dispatch impression event for holdout decision for isFeatureEnabled', async () => { + const processSpy = vi.spyOn(eventProcessor, 'process'); + + vi.spyOn(decisionService, 'getVariationForFeature').mockReturnValue({ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }); + + const result = optimizely.isFeatureEnabled('flag_1', 'test_user', {}); + + expect(result).toBe(false); + + expect(eventProcessor.process).toHaveBeenCalledOnce(); + const event = processSpy.mock.calls[0][0] as ImpressionEvent; + + expect(event.type).toBe('impression'); + expect(event.ruleKey).toBe('holdout_test_key'); + expect(event.ruleType).toBe('holdout'); + expect(event.enabled).toBe(false); + }); + + it('should send correct decision notification for holdout decision', async () => { + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.flagKey).toBe('flag_1'); + expect(decision.enabled).toBe(false); + expect(decision.variationKey).toBe('holdout_variation_key'); + expect(decision.ruleKey).toBe('holdout_test_key'); + + // Verify decision notification was sent + expect(flagNotificationSpy).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + flagKey: 'flag_1', + enabled: false, + variationKey: 'holdout_variation_key', + ruleKey: 'holdout_test_key', + variables: expect.any(Object), + reasons: expect.any(Array), + decisionEventDispatched: true, + }), + }); + + expect(activateNotificationSpy).toHaveBeenCalledWith(expect.objectContaining({ + experiment: null, + holdout: projectConfig.holdouts[0], + userId: 'test_user', + attributes: { country: 'US' }, + variation: projectConfig.holdouts[0].variations[0] + })); + }); + + it('should handle holdout with included flags', async () => { + // Modify holdout to include specific flag + const modifiedHoldout = { ...projectConfig.holdouts[0] }; + modifiedHoldout.includedFlags = ['1001']; // flag_1 ID from test datafile + projectConfig.holdouts = [modifiedHoldout]; + + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: modifiedHoldout.variations[0], + experiment: modifiedHoldout, + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.enabled).toBe(false); + expect(decision.ruleKey).toBe('holdout_test_key'); + expect(decision.variationKey).toBe('holdout_variation_key'); + + // Verify notification shows holdout details + expect(flagNotificationSpy).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + flagKey: 'flag_1', + enabled: false, + ruleKey: 'holdout_test_key', + }), + }); + + expect(activateNotificationSpy).toHaveBeenCalledWith(expect.objectContaining({ + experiment: null, + holdout: modifiedHoldout, + userId: 'test_user', + attributes: { country: 'US' }, + variation: modifiedHoldout.variations[0] + })); + }); + + it('should handle holdout with excluded flags', async () => { + // Modify holdout to exclude specific flag + const modifiedHoldout = { ...projectConfig.holdouts[0] }; + modifiedHoldout.excludedFlags = ['1001']; // flag_1 ID from test datafile + projectConfig.holdouts = [modifiedHoldout]; + + // Mock normal feature test behavior for excluded flag + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.variationIdMap['5003'], + experiment: projectConfig.experimentKeyMap['exp_3'], + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: { country: 'BD', age: 80 }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.enabled).toBe(true); + expect(decision.ruleKey).toBe('exp_3'); + expect(decision.variationKey).toBe('variation_3'); + + // Verify notification shows normal experiment details (not holdout) + expect(flagNotificationSpy).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'BD', age: 80 }, + decisionInfo: expect.objectContaining({ + flagKey: 'flag_1', + enabled: true, + ruleKey: 'exp_3', + }), + }); + + expect(activateNotificationSpy).toHaveBeenCalledWith(expect.objectContaining({ + experiment: projectConfig.experimentKeyMap['exp_3'], + holdout: null, + userId: 'test_user', + attributes: { country: 'BD', age: 80 }, + variation: projectConfig.variationIdMap['5003'] + })); + }); + + it('should handle multiple holdouts with correct priority', async () => { + // Setup multiple holdouts + const holdout1 = { ...projectConfig.holdouts[0] }; + holdout1.excludedFlags = ['1001']; // exclude flag_1 + + const holdout2 = { + id: 'holdout_test_id_2', + key: 'holdout_test_key_2', + status: 'Running', + includedFlags: ['1001'], // include flag_1 + excludedFlags: [], + audienceIds: [], + audienceConditions: [], + variations: [ + { + id: 'holdout_variation_id_2', + key: 'holdout_variation_key_2', + variables: [], + featureEnabled: false, + }, + ], + trafficAllocation: [ + { + entityId: 'holdout_variation_id_2', + endOfRange: 10000, + }, + ], + }; + + projectConfig.holdouts = [holdout1, holdout2]; + + // Mock that holdout2 takes priority due to includedFlags + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: holdout2.variations[0], + experiment: holdout2, + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.enabled).toBe(false); + expect(decision.ruleKey).toBe('holdout_test_key_2'); + expect(decision.variationKey).toBe('holdout_variation_key_2'); + + // Verify notification shows details of selected holdout + expect(flagNotificationSpy).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + flagKey: 'flag_1', + enabled: false, + ruleKey: 'holdout_test_key_2', + }), + }); + + expect(activateNotificationSpy).toHaveBeenCalledWith(expect.objectContaining({ + experiment: null, + holdout: holdout2, + userId: 'test_user', + attributes: { country: 'US' }, + variation: holdout2.variations[0] + })); + }); + + it('should respect sendFlagDecisions setting for holdout events - false', async () => { + // Set sendFlagDecisions to false + projectConfig.sendFlagDecisions = false; + + const mockEventDispatcher = { + dispatchEvent: vi.fn(() => Promise.resolve({ statusCode: 200 })), + }; + const eventProcessor = getForwardingEventProcessor(mockEventDispatcher); + const processSpy = vi.spyOn(eventProcessor, 'process'); + + const projectConfigManager = getMockProjectConfigManager({ + initConfig: projectConfig, + }); + + const optimizelyWithConfig = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator, + logger, + odpManager, + disposable: true, + cmabService: {} as any + }); + + // Add notification listener + const notificationSpyLocal = vi.fn(); + optimizelyWithConfig.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + notificationSpyLocal + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const decisionServiceLocal = optimizelyWithConfig.decisionService; + vi.spyOn(decisionServiceLocal, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely: optimizelyWithConfig, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + await optimizelyWithConfig.decideAsync(user, 'flag_1', []); + + // Impression event should still be dispatched for holdouts even when sendFlagDecisions is false + expect(processSpy).toHaveBeenCalledOnce(); + + // Verify notification shows decisionEventDispatched: true + expect(notificationSpyLocal).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + decisionEventDispatched: true, + }), + }); + }); + + it('should respect sendFlagDecisions setting for holdout events - true', async () => { + // Set sendFlagDecisions to true + projectConfig.sendFlagDecisions = true; + + const mockEventDispatcher = { + dispatchEvent: vi.fn(() => Promise.resolve({ statusCode: 200 })), + }; + const eventProcessor = getForwardingEventProcessor(mockEventDispatcher); + const processSpy = vi.spyOn(eventProcessor, 'process'); + + const projectConfigManager = getMockProjectConfigManager({ + initConfig: projectConfig, + }); + + const optimizelyWithConfig = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator, + logger, + odpManager, + disposable: true, + cmabService: {} as any + }); + + // Add notification listener + const notificationSpyLocal = vi.fn(); + optimizelyWithConfig.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + notificationSpyLocal + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const decisionServiceLocal = optimizelyWithConfig.decisionService; + vi.spyOn(decisionServiceLocal, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely: optimizelyWithConfig, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + await optimizelyWithConfig.decideAsync(user, 'flag_1', []); + + // Impression event should be dispatched for holdouts + expect(processSpy).toHaveBeenCalledOnce(); + + // Verify notification shows decisionEventDispatched: true + expect(notificationSpyLocal).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + decisionEventDispatched: true, + }), + }); + }); + + it('should return correct variable values for holdout decision', async () => { + vi.spyOn(decisionService, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + const decision = await optimizely.decideAsync(user, 'flag_1', []); + + expect(decision.enabled).toBe(false); + expect(decision.variables).toBeDefined(); + expect(typeof decision.variables).toBe('object'); + + // Verify notification includes variable information + expect(flagNotificationSpy).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + variables: expect.any(Object), + flagKey: 'flag_1', + enabled: false, + }), + }); + + expect(activateNotificationSpy).toHaveBeenCalledWith(expect.objectContaining({ + experiment: null, + holdout: projectConfig.holdouts[0], + userId: 'test_user', + attributes: { country: 'US' }, + variation: projectConfig.holdouts[0].variations[0] + })); + }); + + it('should handle disable decision event option for holdout', async () => { + const mockEventDispatcher = { + dispatchEvent: vi.fn(() => Promise.resolve({ statusCode: 200 })), + }; + const eventProcessor = getForwardingEventProcessor(mockEventDispatcher); + const processSpy = vi.spyOn(eventProcessor, 'process'); + + const projectConfigManager = getMockProjectConfigManager({ + initConfig: projectConfig, + }); + + const optimizelyWithEventProcessor = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator, + logger, + odpManager, + disposable: true, + cmabService: {} as any + }); + + // Add notification listener + const notificationSpyLocal = vi.fn(); + optimizelyWithEventProcessor.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + notificationSpyLocal + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const decisionServiceLocal = optimizelyWithEventProcessor.decisionService; + vi.spyOn(decisionServiceLocal, 'resolveVariationsForFeatureList').mockImplementation(() => { + return Value.of('async', [{ + error: false, + result: { + variation: projectConfig.holdouts[0].variations[0], + experiment: projectConfig.holdouts[0], + decisionSource: DECISION_SOURCES.HOLDOUT, + }, + reasons: [], + }]); + }); + + const user = new OptimizelyUserContext({ + optimizely: optimizelyWithEventProcessor, + userId: 'test_user', + attributes: { country: 'US' }, + }); + + const decision = await optimizelyWithEventProcessor.decideAsync(user, 'flag_1', [OptimizelyDecideOption.DISABLE_DECISION_EVENT]); + + expect(decision.enabled).toBe(false); + expect(decision.ruleKey).toBe('holdout_test_key'); + + // No impression event should be dispatched + expect(processSpy).not.toHaveBeenCalled(); + + // Verify notification shows decisionEventDispatched: false + expect(notificationSpyLocal).toHaveBeenCalledWith({ + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: 'test_user', + attributes: { country: 'US' }, + decisionInfo: expect.objectContaining({ + decisionEventDispatched: false, + }), + }); + }); + }); + + it('should flush eventProcessor and odpManager on flushImmediately()', async () => { + const projectConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + const odpManager = extractOdpManager(createOdpManager({})); + + const optimizely = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + jsonSchemaValidator, + logger, + eventProcessor, + odpManager, + disposable: true, + cmabService: {} as any + }); + + odpManager?.updateConfig({ integrated: false }); + await optimizely.onReady(); + + const eventProcessorFlushSpy = vi.spyOn(eventProcessor, 'flushImmediately').mockResolvedValue(Promise.resolve()); + const odpManagerFlushSpy = vi.spyOn(odpManager!, 'flushImmediately').mockResolvedValue(Promise.resolve()); + + await optimizely.flushImmediately(); + + expect(eventProcessorFlushSpy).toHaveBeenCalled(); + expect(odpManagerFlushSpy).toHaveBeenCalled(); + + expect(optimizely.isRunning()).toBe(true); + }); +}); diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js new file mode 100644 index 000000000..d3f350bba --- /dev/null +++ b/lib/optimizely/index.tests.js @@ -0,0 +1,9734 @@ +/** + * Copyright 2016-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert, expect } from 'chai'; +import sinon from 'sinon'; +import { sprintf } from '../utils/fns'; +import { NOTIFICATION_TYPES } from '../notification_center/type'; +import Optimizely, { INVALID_ATTRIBUTES, INVALID_IDENTIFIER } from './'; +import OptimizelyUserContext from '../optimizely_user_context'; +import { OptimizelyDecideOption } from '../shared_types'; +import AudienceEvaluator from '../core/audience_evaluator'; +import * as bucketer from '../core/bucketer'; +import * as enums from '../utils/enums'; +import fns from '../utils/fns'; +import * as decisionService from '../core/decision_service'; +import * as jsonSchemaValidator from '../utils/json_schema_validator'; +import * as projectConfig from '../project_config/project_config'; +import testData from '../tests/test_data'; +import { getForwardingEventProcessor } from '../event_processor/event_processor_factory'; +import { createNotificationCenter } from '../notification_center'; +import { createProjectConfig } from '../project_config/project_config'; +import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; +import { DECISION_NOTIFICATION_TYPES } from '../notification_center/type'; +import { + FEATURE_NOT_ENABLED_FOR_USER, + INVALID_CLIENT_ENGINE, + INVALID_DEFAULT_DECIDE_OPTIONS, + NOT_ACTIVATING_USER, + VALID_USER_PROFILE_SERVICE, +} from 'log_message'; +import { + NOT_TRACKING_USER, + EVENT_KEY_NOT_FOUND, + INVALID_EXPERIMENT_KEY, + SERVICE_STOPPED_BEFORE_RUNNING +} from 'error_message'; + +import { ONREADY_TIMEOUT, INSTANCE_CLOSED } from './'; +import { + AUDIENCE_EVALUATION_RESULT_COMBINED, + USER_NOT_IN_EXPERIMENT, + FEATURE_HAS_NO_EXPERIMENTS, + USER_HAS_NO_VARIATION, + USER_HAS_VARIATION, + USER_NOT_BUCKETED_INTO_TARGETING_RULE, + USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, + USER_IN_ROLLOUT, + USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, + FORCED_BUCKETING_FAILED, + USER_HAS_FORCED_VARIATION, + USER_FORCED_IN_VARIATION, + RETURNING_STORED_VARIATION, + EXPERIMENT_NOT_RUNNING, +} from '../core/decision_service'; + +import { USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP } from '../core/bucketer'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { holdout } from '../feature_toggle'; + +var LOG_LEVEL = enums.LOG_LEVEL; +var DECISION_SOURCES = enums.DECISION_SOURCES; +var DECISION_MESSAGES = enums.DECISION_MESSAGES; +var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; + +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); + +const getMockEventDispatcher = () => { + const dispatcher = { + dispatchEvent: sinon.spy(() => Promise.resolve({ statusCode: 200 })), + } + return dispatcher; +} + +const getMockEventProcessor = (notificationCenter) => { + return getForwardingEventProcessor(getMockEventDispatcher(), notificationCenter); +} + +const getMockErrorNotifier = () => { + return { + notify: sinon.spy(), + } +}; + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +const getOptlyInstance = ({ datafileObj, defaultDecideOptions }) => { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(datafileObj), + }); + const eventDispatcher = getMockEventDispatcher(); + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + + const errorNotifier = getMockErrorNotifier(); + + const notificationCenter = createNotificationCenter({ logger: createdLogger, errorNotifier }); + var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + + const optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + errorNotifier, + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: defaultDecideOptions || [], + notificationCenter, + }); + + sinon.stub(notificationCenter, 'sendNotifications'); + + return { optlyInstance, eventProcessor, eventDispatcher, notificationCenter, errorNotifier, createdLogger } +} + +describe('lib/optimizely', function() { + var clock; + beforeEach(function() { + // sinon.stub(eventDispatcher, 'dispatchEvent'); + clock = sinon.useFakeTimers(new Date()); + }); + + afterEach(function() { + // eventDispatcher.dispatchEvent.restore(); + clock.restore(); + }); + + describe('constructor', function() { + var stubEventDispatcher = { + dispatchEvent: function() { + return Promise.resolve(null); + }, + }; + var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + var notificationCenter = createNotificationCenter({ logger: createdLogger }); + var eventProcessor = getForwardingEventProcessor(stubEventDispatcher); + beforeEach(function() { + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + }); + + afterEach(function() { + createdLogger.debug.restore(); + createdLogger.info.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + }); + + describe('constructor', function() { + it('should log if the client engine passed in is invalid', function() { + new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + eventDispatcher: stubEventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + sinon.assert.called(createdLogger.info); + + assert.deepEqual(createdLogger.info.args[0], [INVALID_CLIENT_ENGINE, undefined]); + }); + + it('should log if the defaultDecideOptions passed in are invalid', function() { + new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + clientEngine: 'node-sdk', + + eventDispatcher: stubEventDispatcher, + logger: createdLogger, + defaultDecideOptions: 'invalid_options', + notificationCenter, + eventProcessor, + }); + + sinon.assert.called(createdLogger.debug); + assert.deepEqual(createdLogger.debug.args[0], [INVALID_DEFAULT_DECIDE_OPTIONS]); + }); + + it('should allow passing `react-sdk` as the clientEngine', function() { + var instance = new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + clientEngine: 'react-sdk', + eventDispatcher: stubEventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + assert.strictEqual(instance.clientEngine, 'react-sdk'); + }); + + describe('when a user profile service is provided', function() { + beforeEach(function() { + sinon.stub(decisionService, 'createDecisionService'); + }); + + afterEach(function() { + decisionService.createDecisionService.restore(); + }); + + it('should validate and pass the user profile service to the decision service', function() { + var userProfileServiceInstance = { + lookup: function() {}, + save: function() {}, + }; + + const cmabService = {}; + + new Optimizely({ + clientEngine: 'node-sdk', + logger: createdLogger, + projectConfigManager: getMockProjectConfigManager(), + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: userProfileServiceInstance, + notificationCenter, + cmabService, + eventProcessor, + }); + + sinon.assert.calledWith(decisionService.createDecisionService, { + userProfileService: userProfileServiceInstance, + userProfileServiceAsync: undefined, + logger: createdLogger, + cmabService, + UNSTABLE_conditionEvaluators: undefined, + }); + + sinon.assert.calledWith( + createdLogger.info, + VALID_USER_PROFILE_SERVICE, + ); + }); + + it('should pass in a null user profile to the decision service if the provided user profile is invalid', function() { + var invalidUserProfile = { + save: function() {}, + }; + + const cmabService = {}; + + new Optimizely({ + clientEngine: 'node-sdk', + logger: createdLogger, + projectConfigManager: getMockProjectConfigManager(), + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: invalidUserProfile, + notificationCenter, + eventProcessor, + cmabService, + }); + + sinon.assert.calledWith(decisionService.createDecisionService, { + userProfileService: undefined, + userProfileServiceAsync: undefined, + logger: createdLogger, + UNSTABLE_conditionEvaluators: undefined, + cmabService, + }); + + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual( + // logMessage, + // "USER_PROFILE_SERVICE_VALIDATOR: Provided user profile service instance is in an invalid format: Missing function 'lookup'." + // ); + sinon.assert.called(createdLogger.warn); + }); + }); + }); + }); + + describe('APIs', function() { + var optlyInstance; + var bucketStub; + var fakeDecisionResponse; + var eventDispatcher = getMockEventDispatcher(); + var notificationCenter = createNotificationCenter({ logger: createdLogger }); + var eventProcessor = getForwardingEventProcessor(eventDispatcher, notificationCenter); + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + eventProcessor, + notificationCenter, + }); + + bucketStub = sinon.stub(bucketer, 'bucket'); + + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + bucketer.bucket.restore(); + + createdLogger.debug.restore(); + createdLogger.info.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + fns.uuid.restore(); + }); + + describe('#activate', function() { + it('should call bucketer and dispatchEvent with proper args and return variation key', function() { + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var variation = optlyInstance.activate('testExperiment', 'testUser'); + assert.strictEqual(variation, 'variation'); + + sinon.assert.calledOnce(bucketer.bucket); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '4', + experiment_id: '111127', + variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '4', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should dispatch proper params for null value attributes', function() { + fakeDecisionResponse = { + result: '122229', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', { + browser_type: 'firefox', + test_null_attribute: null, + }); + assert.strictEqual(activate, 'variationWithAudience'); + + sinon.assert.calledOnce(bucketer.bucket); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '5', + experiment_id: '122227', + variation_id: '122229', + metadata: { + flag_key: '', + rule_key: 'testExperimentWithAudiences', + rule_type: 'experiment', + variation_key: 'variationWithAudience', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '5', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call bucketer and dispatchEvent with proper args and return variation key if user is in audience', function() { + fakeDecisionResponse = { + result: '122229', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', { browser_type: 'firefox' }); + assert.strictEqual(activate, 'variationWithAudience'); + + sinon.assert.calledOnce(bucketer.bucket); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '5', + experiment_id: '122227', + variation_id: '122229', + metadata: { + flag_key: '', + rule_key: 'testExperimentWithAudiences', + rule_type: 'experiment', + variation_key: 'variationWithAudience', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '5', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call activate and dispatchEvent with typed attributes and return variation key', function() { + fakeDecisionResponse = { + result: '122229', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', { + browser_type: 'firefox', + boolean_key: true, + integer_key: 10, + double_key: 3.14, + }); + assert.strictEqual(activate, 'variationWithAudience'); + + sinon.assert.calledOnce(bucketer.bucket); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '5', + experiment_id: '122227', + variation_id: '122229', + metadata: { + flag_key: '', + rule_key: 'testExperimentWithAudiences', + rule_type: 'experiment', + variation_key: 'variationWithAudience', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '5', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + { + entity_id: '323434545', + key: 'boolean_key', + type: 'custom', + value: true, + }, + { + entity_id: '616727838', + key: 'integer_key', + type: 'custom', + value: 10, + }, + { + entity_id: '808797686', + key: 'double_key', + type: 'custom', + value: 3.14, + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + describe('when experiment_bucket_map attribute is present', function() { + it('should call activate and respect attribute experiment_bucket_map', function() { + fakeDecisionResponse = { + result: '111128', // id of "control" variation + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('testExperiment', 'testUser', { + $opt_experiment_bucket_map: { + '111127': { + variation_id: '111129', // id of "variation" variation + }, + }, + }); + + assert.strictEqual(activate, 'variation'); + sinon.assert.notCalled(bucketer.bucket); + }); + }); + + it('should call bucketer and dispatchEvent with proper args and return variation key if user is in grouped experiment', function() { + fakeDecisionResponse = { + result: '662', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('groupExperiment2', 'testUser'); + assert.strictEqual(activate, 'var2exp2'); + + sinon.assert.calledOnce(bucketer.bucket); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '2', + experiment_id: '443', + variation_id: '662', + metadata: { + flag_key: '', + rule_key: 'groupExperiment2', + rule_type: 'experiment', + variation_key: 'var2exp2', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '2', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call bucketer and dispatchEvent with proper args and return variation key if user is in grouped experiment and is in audience', function() { + fakeDecisionResponse = { + result: '552', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('groupExperiment1', 'testUser', { browser_type: 'firefox' }); + assert.strictEqual(activate, 'var2exp1'); + + sinon.assert.calledOnce(bucketer.bucket); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '1', + experiment_id: '442', + variation_id: '552', + metadata: { + flag_key: '', + rule_key: 'groupExperiment1', + rule_type: 'experiment', + variation_key: 'var2exp1', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '1', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should not make a dispatch event call if variation ID is null', function() { + fakeDecisionResponse = { + result: null, + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + assert.isNull(optlyInstance.activate('testExperiment', 'testUser')); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.called(createdLogger.info); + + sinon.assert.calledWithExactly( + createdLogger.info, + NOT_ACTIVATING_USER, + 'testUser', + 'testExperiment' + ); + }); + + it('should return null if user is not in audience and user is not in group', function() { + assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'testUser', { browser_type: 'chrome' })); + + sinon.assert.calledWithExactly( + createdLogger.info, + NOT_ACTIVATING_USER, + 'testUser', + 'testExperimentWithAudiences' + ); + }); + + it('should return null if user is not in audience and user is in group', function() { + assert.isNull(optlyInstance.activate('groupExperiment1', 'testUser', { browser_type: 'chrome' })); + + sinon.assert.calledWithExactly( + createdLogger.info, + NOT_ACTIVATING_USER, + 'testUser', + 'groupExperiment1' + ); + }); + + it('should return null if experiment is not running', function() { + assert.isNull(optlyInstance.activate('testExperimentNotRunning', 'testUser')); + + sinon.assert.calledWithExactly( + createdLogger.info, + NOT_ACTIVATING_USER, + 'testUser', + 'testExperimentNotRunning' + ); + }); + + it('should throw an error for invalid user ID', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + assert.isNull(optlyInstance.activate('testExperiment', null)); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + + sinon.assert.calledOnce(errorNotifier.notify); + + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + + // sinon.assert.calledTwice(createdLogger.log); + + // var logMessage1 = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage1, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + + // var logMessage2 = buildLogMessageFromArgs(createdLogger.log.args[1]); + // assert.strictEqual( + // logMessage2, + // sprintf(NOT_ACTIVATING_USER, 'OPTIMIZELY', 'null', 'testExperiment') + // ); + }); + + it('should log an error for invalid experiment key', function() { + assert.isNull(optlyInstance.activate('invalidExperimentKey', 'testUser')); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + + sinon.assert.calledWithExactly( + createdLogger.debug, + INVALID_EXPERIMENT_KEY, + 'invalidExperimentKey' + ); + + sinon.assert.calledWithExactly( + createdLogger.info, + NOT_ACTIVATING_USER, + 'testUser', + 'invalidExperimentKey' + ); + }); + + it('should throw an error for invalid attributes', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + sinon.stub(createdLogger, 'info'); + + assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'testUser', [])); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledOnce(errorNotifier.notify); + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + + // sinon.assert.calledTwice(createdLogger.log); + // var logMessage1 = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage1, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + // var logMessage2 = buildLogMessageFromArgs(createdLogger.log.args[1]); + // assert.strictEqual( + // logMessage2, + // sprintf(NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentWithAudiences') + // ); + sinon.assert.calledWithExactly( + createdLogger.info, + NOT_ACTIVATING_USER, + 'testUser', + 'testExperimentWithAudiences' + ); + }); + + it('should activate when logger is in DEBUG mode', function() { + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + var instance = new Optimizely({ + projectConfigManager: mockConfigManager, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createLogger({ + logLevel: enums.LOG_LEVEL.DEBUG, + logToConsole: false, + }), + isValidInstance: true, + eventBatchSize: 1, + eventProcessor, + notificationCenter, + }); + + var variation = instance.activate('testExperiment', 'testUser'); + assert.strictEqual(variation, 'variation'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + }); + + describe('whitelisting', function() { + beforeEach(function() { + sinon.spy(Optimizely.prototype, 'validateInputs'); + }); + + afterEach(function() { + Optimizely.prototype.validateInputs.restore(); + }); + + it('should return forced variation after experiment status check and before audience check', function() { + var activate = optlyInstance.activate('testExperiment', 'user1'); + assert.strictEqual(activate, 'control'); + + sinon.assert.calledTwice(Optimizely.prototype.validateInputs); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '4', + experiment_id: '111127', + variation_id: '111128', + }, + ], + events: [ + { + entity_id: '4', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'user1', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + }); + }); + + }); + + describe('#track', function() { + it("should dispatch an event when no attributes are provided and the event's experiment is untargeted", function() { + optlyInstance.track('testEvent', 'testUser'); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it("should dispatch an event when empty attributes are provided and the event's experiment is untargeted", function() { + optlyInstance.track('testEvent', 'testUser', {}); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it("should dispatch an event when attributes are provided and the event's experiment is untargeted", function() { + optlyInstance.track('testEvent', 'testUser', { browser_type: 'safari' }); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'safari', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it("should dispatch an event when no attributes are provided and the event's experiment is targeted", function() { + optlyInstance.track('testEventWithAudiences', 'testUser'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111097', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithAudiences', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it("should dispatch an event when empty attributes are provided and the event's experiment is targeted", function() { + optlyInstance.track('testEventWithAudiences', 'testUser'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111097', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithAudiences', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call dispatchEvent with proper args when including null value attributes', function() { + optlyInstance.track('testEventWithAudiences', 'testUser', { + browser_type: 'firefox', + test_null_attribute: null, + }); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111097', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithAudiences', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call dispatchEvent with proper args when including attributes', function() { + optlyInstance.track('testEventWithAudiences', 'testUser', { browser_type: 'firefox' }); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111097', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithAudiences', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call bucketer and dispatchEvent with proper args when including event tags', function() { + optlyInstance.track('testEvent', 'testUser', undefined, { eventTag: 'chill' }); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + tags: { + eventTag: 'chill', + }, + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call dispatchEvent with proper args when including event tags and revenue', function() { + optlyInstance.track('testEvent', 'testUser', undefined, { revenue: 4200, eventTag: 'chill' }); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + revenue: 4200, + tags: { + revenue: 4200, + eventTag: 'chill', + }, + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should call dispatchEvent with proper args when including event tags and null event tag values and revenue', function() { + optlyInstance.track('testEvent', 'testUser', undefined, { + revenue: 4200, + eventTag: 'chill', + testNullEventTag: null, + }); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + revenue: 4200, + tags: { + revenue: 4200, + eventTag: 'chill', + }, + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should not call dispatchEvent when including invalid event value', function() { + optlyInstance.track('testEvent', 'testUser', undefined, '4200'); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledOnce(createdLogger.error); + }); + + it('should track a user for an experiment not running', function() { + optlyInstance.track('testEventWithExperimentNotRunning', 'testUser'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111099', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithExperimentNotRunning', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should track a user when user is not in the audience of the experiment', function() { + optlyInstance.track('testEventWithAudiences', 'testUser', { browser_type: 'chrome' }); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111097', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithAudiences', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'chrome', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should track a user when the event has no associated experiments', function() { + optlyInstance.track('testEventWithoutExperiments', 'testUser'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111098', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithoutExperiments', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should only send one conversion event when the event is attached to multiple experiments', function() { + optlyInstance.track('testEventWithMultipleExperiments', 'testUser', { browser_type: 'firefox' }); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedObj = { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111100', + timestamp: Math.round(new Date().getTime()), + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEventWithMultipleExperiments', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; + assert.deepEqual(eventDispatcherCall[0], expectedObj); + }); + + it('should throw an error for invalid user ID', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + sinon.stub(createdLogger, 'info'); + + optlyInstance.track('testEvent', null); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + + sinon.assert.calledOnce(errorNotifier.notify); + + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + }); + + it('should log a warning for an event key that is not in the datafile and a warning for not tracking user', function() { + const { optlyInstance, errorNotifier, createdLogger, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + sinon.stub(createdLogger, 'warn'); + + optlyInstance.track('invalidEventKey', 'testUser'); + + sinon.assert.calledWithExactly( + createdLogger.warn, + EVENT_KEY_NOT_FOUND, + 'invalidEventKey' + ); + + sinon.assert.calledWithExactly( + createdLogger.warn, + NOT_TRACKING_USER, + 'testUser' + ); + + // sinon.assert.notCalled(errorHandler.handleError); + }); + + it('should throw an error for invalid attributes', function() { + optlyInstance.track('testEvent', 'testUser', []); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + + // sinon.assert.calledOnce(errorHandler.handleError); + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + + it('should not throw an error for an event key without associated experiment IDs', function() { + optlyInstance.track('testEventWithoutExperiments', 'testUser'); + // sinon.assert.notCalled(errorHandler.handleError); + }); + + it('should track when logger is in DEBUG mode', function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + var instance = new Optimizely({ + projectConfigManager: mockConfigManager, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createLogger({ + logLevel: enums.LOG_LEVEL.DEBUG, + logToConsole: false, + }), + isValidInstance: true, + eventBatchSize: 1, + eventProcessor, + notificationCenter, + }); + + instance.track('testEvent', 'testUser'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + }); + }); + + describe('#getVariation', function() { + it('should call bucketer and return variation key', function() { + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var variation = optlyInstance.getVariation('testExperiment', 'testUser'); + + assert.strictEqual(variation, 'variation'); + + sinon.assert.calledOnce(bucketer.bucket); + }); + + it('should call bucketer and return variation key with attributes', function() { + fakeDecisionResponse = { + result: '122229', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var getVariation = optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', { + browser_type: 'firefox', + }); + + assert.strictEqual(getVariation, 'variationWithAudience'); + + sinon.assert.calledOnce(bucketer.bucket); + }); + + it('should return null if user is not in audience or experiment is not running', function() { + var getVariationReturnsNull1 = optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', {}); + var getVariationReturnsNull2 = optlyInstance.getVariation('testExperimentNotRunning', 'testUser'); + + assert.isNull(getVariationReturnsNull1); + assert.isNull(getVariationReturnsNull2); + + sinon.assert.notCalled(bucketer.bucket); + }); + + it('should throw an error for invalid user ID', function() { + const { optlyInstance, errorNotifier, createdLogger, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + var getVariationWithError = optlyInstance.getVariation('testExperiment', null); + + assert.isNull(getVariationWithError); + + sinon.assert.calledOnce(errorNotifier.notify); + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + }); + + it('should log an error for invalid experiment key', function() { + var getVariationWithError = optlyInstance.getVariation('invalidExperimentKey', 'testUser'); + assert.isNull(getVariationWithError); + + sinon.assert.calledWithExactly( + createdLogger.debug, + INVALID_EXPERIMENT_KEY, + 'invalidExperimentKey' + ); + }); + + it('should throw an error for invalid attributes', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + var getVariationWithError = optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', []); + + assert.isNull(getVariationWithError); + + sinon.assert.calledOnce(errorNotifier.notify); + + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + + describe('whitelisting', function() { + beforeEach(function() { + sinon.spy(Optimizely.prototype, 'validateInputs'); + }); + + afterEach(function() { + Optimizely.prototype.validateInputs.restore(); + }); + + it('should return forced variation after experiment status check and before audience check', function() { + var getVariation = optlyInstance.getVariation('testExperiment', 'user1'); + assert.strictEqual(getVariation, 'control'); + + sinon.assert.calledOnce(Optimizely.prototype.validateInputs); + }); + }); + + describe('order of bucketing operations', function() { + it('should properly follow the order of bucketing operations', function() { + // Order of operations is preconditions > experiment is running > whitelisting > audience eval > variation bucketing + fakeDecisionResponse = { + result: '122228', // returns the control variation + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + + // invalid user, running experiment + assert.isNull(optlyInstance.activate('testExperiment', 123)); + + // valid user, experiment not running, whitelisted + assert.isNull(optlyInstance.activate('testExperimentNotRunning', 'user1')); + + // valid user, experiment running, not whitelisted, does not meet audience conditions + assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'user3')); + + // valid user, experiment running, not whitelisted, meets audience conditions + assert.strictEqual( + optlyInstance.activate('testExperimentWithAudiences', 'user3', { browser_type: 'firefox' }), + 'controlWithAudience' + ); + + // valid user, running experiment, whitelisted, does not meet audience conditions + // expect user to be forced into `variationWithAudience` through whitelisting + assert.strictEqual( + optlyInstance.activate('testExperimentWithAudiences', 'user2', { browser_type: 'chrome' }), + 'variationWithAudience' + ); + + // valid user, running experiment, whitelisted, meets audience conditions + // expect user to be forced into `variationWithAudience (122229)` through whitelisting + assert.strictEqual( + optlyInstance.activate('testExperimentWithAudiences', 'user2', { browser_type: 'firefox' }), + 'variationWithAudience' + ); + }); + }); + }); + + describe('#getForcedVariation', function() { + it('should return null when set has not been called', function() { + var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); + assert.strictEqual(forcedVariation, null); + }); + + it('should return null with a null experimentKey', function() { + var forcedVariation = optlyInstance.getForcedVariation(null, 'user1'); + assert.strictEqual(forcedVariation, null); + + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); + }); + + it('should return null with an undefined experimentKey', function() { + var forcedVariation = optlyInstance.getForcedVariation(undefined, 'user1'); + assert.strictEqual(forcedVariation, null); + + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); + }); + + it('should return null with a null userId', function() { + var forcedVariation = optlyInstance.getForcedVariation('testExperiment', null); + assert.strictEqual(forcedVariation, null); + + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + }); + + it('should return null with an undefined userId', function() { + var forcedVariation = optlyInstance.getForcedVariation('testExperiment', undefined); + assert.strictEqual(forcedVariation, null); + + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + }); + }); + + describe('#setForcedVariation', function() { + it('should be able to set a forced variation', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); + assert.strictEqual(didSetVariation, true); + }); + + it('should override bucketing in optlyInstance.getVariation', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); + assert.strictEqual(didSetVariation, true); + + var variation = optlyInstance.getVariation('testExperiment', 'user1', {}); + assert.strictEqual(variation, 'control'); + }); + + it('should be able to set and get forced variation', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); + assert.strictEqual(didSetVariation, true); + + var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); + assert.strictEqual(forcedVariation, 'control'); + }); + + it('should be able to set, unset, and get forced variation', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); + assert.strictEqual(didSetVariation, true); + + var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); + assert.strictEqual(forcedVariation, 'control'); + + var didSetVariation2 = optlyInstance.setForcedVariation('testExperiment', 'user1', null); + assert.strictEqual(didSetVariation2, true); + + var forcedVariation2 = optlyInstance.getForcedVariation('testExperiment', 'user1'); + assert.strictEqual(forcedVariation2, null); + }); + + it('should be able to set multiple experiments for one user', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); + assert.strictEqual(didSetVariation, true); + + var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'user1', 'variationLaunched'); + assert.strictEqual(didSetVariation2, true); + + var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); + assert.strictEqual(forcedVariation, 'control'); + + var forcedVariation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'user1'); + assert.strictEqual(forcedVariation2, 'variationLaunched'); + }); + + it('should not set an invalid variation', function() { + var didSetVariation = optlyInstance.setForcedVariation( + 'testExperiment', + 'user1', + 'definitely_not_valid_variation_key' + ); + assert.strictEqual(didSetVariation, false); + }); + + it('should not set an invalid experiment', function() { + var didSetVariation = optlyInstance.setForcedVariation('definitely_not_valid_exp_key', 'user1', 'control'); + assert.strictEqual(didSetVariation, false); + }); + + it('should return null for user has no forced variation for experiment', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); + assert.strictEqual(didSetVariation, true); + + var forcedVariation = optlyInstance.getForcedVariation('testExperimentLaunched', 'user1'); + assert.strictEqual(forcedVariation, null); + }); + + it('should return false for a null experimentKey', function() { + var didSetVariation = optlyInstance.setForcedVariation(null, 'user1', 'control'); + assert.strictEqual(didSetVariation, false); + + // var setVariationLogMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual( + // setVariationLogMessage, + // sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key') + // ); + }); + + it('should return false for an undefined experimentKey', function() { + var didSetVariation = optlyInstance.setForcedVariation(undefined, 'user1', 'control'); + assert.strictEqual(didSetVariation, false); + + // var setVariationLogMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual( + // setVariationLogMessage, + // sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key') + // ); + }); + + it('should return false for an empty experimentKey', function() { + var didSetVariation = optlyInstance.setForcedVariation('', 'user1', 'control'); + assert.strictEqual(didSetVariation, false); + + // var setVariationLogMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual( + // setVariationLogMessage, + // sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key') + // ); + }); + + it('should return false for a null userId', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', null, 'control'); + assert.strictEqual(didSetVariation, false); + + // var setVariationLogMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual( + // setVariationLogMessage, + // sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id') + // ); + }); + + it('should return false for an undefined userId', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', undefined, 'control'); + assert.strictEqual(didSetVariation, false); + + // var setVariationLogMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual( + // setVariationLogMessage, + // sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id') + // ); + }); + + it('should return true for an empty userId', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', '', 'control'); + assert.strictEqual(didSetVariation, true); + }); + + it('should return false for a null variationKey', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', null); + assert.strictEqual(didSetVariation, false); + }); + + it('should return false for an undefined variationKey', function() { + var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', undefined); + assert.strictEqual(didSetVariation, false); + }); + + it('should not override check for not running experiments in getVariation', function() { + var didSetVariation = optlyInstance.setForcedVariation( + 'testExperimentNotRunning', + 'user1', + 'controlNotRunning' + ); + assert.strictEqual(didSetVariation, true); + + var variation = optlyInstance.getVariation('testExperimentNotRunning', 'user1', {}); + assert.strictEqual(variation, null); + }); + }); + + describe('validateInputs', function() { + it('should return true if user ID and attributes are valid', function() { + assert.isTrue(optlyInstance.validateInputs({ user_id: 'testUser' })); + assert.isTrue(optlyInstance.validateInputs({ user_id: '' })); + assert.isTrue(optlyInstance.validateInputs({ user_id: 'testUser' }, { browser_type: 'firefox' })); + // sinon.assert.notCalled(createdLogger.log); + }); + + it('should return false and throw an error if user ID is invalid', function() { + var falseUserIdInput = optlyInstance.validateInputs({ user_id: [] }); + assert.isFalse(falseUserIdInput); + + falseUserIdInput = optlyInstance.validateInputs({ user_id: null }); + assert.isFalse(falseUserIdInput); + + falseUserIdInput = optlyInstance.validateInputs({ user_id: 3.14 }); + assert.isFalse(falseUserIdInput); + + // sinon.assert.calledThrice(errorHandler.handleError); + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + + // sinon.assert.calledThrice(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + }); + + it('should return false and throw an error if attributes are invalid', function() { + const { optlyInstance, errorNotifier} = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + var falseUserIdInput = optlyInstance.validateInputs({ user_id: 'testUser' }, []); + assert.isFalse(falseUserIdInput); + + sinon.assert.calledOnce(errorNotifier.notify); + + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + }); + + describe('should filter out null values', function() { + it('should filter out a null value', function() { + var dict = { test: null }; + var filteredValue = optlyInstance.filterEmptyValues(dict); + assert.deepEqual(filteredValue, {}); + }); + + it('should filter out a undefined value', function() { + var dict = { test: undefined }; + var filteredValue = optlyInstance.filterEmptyValues(dict); + assert.deepEqual(filteredValue, {}); + }); + + it('should filter out a null value, leave a non null one', function() { + var dict = { test: null, test2: 'not_null' }; + var filteredValue = optlyInstance.filterEmptyValues(dict); + assert.deepEqual(filteredValue, { test2: 'not_null' }); + }); + + it('should not filter out a non empty value', function() { + var dict = { test: 'hello' }; + var filteredValue = optlyInstance.filterEmptyValues(dict); + assert.deepEqual(filteredValue, { test: 'hello' }); + }); + }); + + describe('notification listeners', function() { + var activateListener; + var trackListener; + var activateListener2; + var trackListener2; + + beforeEach(function() { + activateListener = sinon.spy(); + trackListener = sinon.spy(); + activateListener2 = sinon.spy(); + trackListener2 = sinon.spy(); + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + sinon.stub(fns, 'currentTimestamp').returns(1509489766569); + }); + + afterEach(function() { + fns.currentTimestamp.restore(); + }); + + it('should call a listener added for activate when activate is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + var variationKey = optlyInstance.activate('testExperiment', 'testUser'); + assert.strictEqual(variationKey, 'variation'); + sinon.assert.calledOnce(activateListener); + }); + + it('should call a listener added for track when track is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + sinon.assert.calledOnce(trackListener); + }); + + it('should not call a removed activate listener when activate is called', function() { + var listenerId = optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.ACTIVATE, + activateListener + ); + optlyInstance.notificationCenter.removeNotificationListener(listenerId); + var variationKey = optlyInstance.activate('testExperiment', 'testUser'); + assert.strictEqual(variationKey, 'variation'); + sinon.assert.notCalled(activateListener); + }); + + it('should not call a removed track listener when track is called', function() { + var listenerId = optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackListener + ); + optlyInstance.notificationCenter.removeNotificationListener(listenerId); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + sinon.assert.notCalled(trackListener); + }); + + it('removeNotificationListener should only remove the listener with the argument ID', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + var trackListenerId = optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.TRACK, + trackListener + ); + optlyInstance.notificationCenter.removeNotificationListener(trackListenerId); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + sinon.assert.calledOnce(activateListener); + }); + + it('should clear all notification listeners when clearAllNotificationListeners is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.clearAllNotificationListeners(); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + + sinon.assert.notCalled(activateListener); + sinon.assert.notCalled(trackListener); + }); + + it('should clear listeners of certain notification type when clearNotificationListeners is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.clearNotificationListeners(NOTIFICATION_TYPES.ACTIVATE); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + + sinon.assert.notCalled(activateListener); + sinon.assert.calledOnce(trackListener); + }); + + it('should not add a listener with an invalid type argument', function() { + var listenerId = optlyInstance.notificationCenter.addNotificationListener( + 'not a notification type', + activateListener + ); + assert.strictEqual(listenerId, -1); + optlyInstance.activate('testExperiment', 'testUser'); + sinon.assert.notCalled(activateListener); + optlyInstance.track('testEvent', 'testUser'); + sinon.assert.notCalled(activateListener); + }); + + it('should call multiple notification listeners for activate when activate is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener2); + optlyInstance.activate('testExperiment', 'testUser'); + sinon.assert.calledOnce(activateListener); + sinon.assert.calledOnce(activateListener2); + }); + + it('should call multiple notification listeners for track when track is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener2); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + sinon.assert.calledOnce(trackListener); + sinon.assert.calledOnce(trackListener2); + }); + + it('should pass the correct arguments to an activate listener when activate is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.activate('testExperiment', 'testUser'); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '4', + experiment_id: '111127', + variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '4', + timestamp: 1509489766569, + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var instanceExperiments = optlyInstance.projectConfigManager.getConfig().experiments; + var expectedArgument = { + experiment: instanceExperiments[0], + holdout: null, + userId: 'testUser', + attributes: undefined, + variation: instanceExperiments[0].variations[1], + logEvent: expectedImpressionEvent, + }; + sinon.assert.calledWith(activateListener, expectedArgument); + }); + + it('should pass the correct arguments to an activate listener when activate is called with attributes', function() { + var attributes = { + browser_type: 'firefox', + }; + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.ACTIVATE, activateListener); + optlyInstance.activate('testExperiment', 'testUser', attributes); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '4', + experiment_id: '111127', + variation_id: '111129', + metadata: { + flag_key: '', + rule_key: 'testExperiment', + rule_type: 'experiment', + variation_key: 'variation', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '4', + timestamp: 1509489766569, + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var instanceExperiments = optlyInstance.projectConfigManager.getConfig().experiments; + var expectedArgument = { + experiment: instanceExperiments[0], + holdout: null, + userId: 'testUser', + attributes: attributes, + variation: instanceExperiments[0].variations[1], + logEvent: expectedImpressionEvent, + }; + sinon.assert.calledWith(activateListener, expectedArgument); + }); + + it('should pass the correct arguments to a track listener when track is called', function() { + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.activate('testExperiment', 'testUser'); + optlyInstance.track('testEvent', 'testUser'); + var expectedConversionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: 1509489766569, + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var expectedArgument = { + eventKey: 'testEvent', + userId: 'testUser', + attributes: undefined, + eventTags: undefined, + logEvent: expectedConversionEvent, + }; + sinon.assert.calledWith(trackListener, expectedArgument); + }); + + it('should pass the correct arguments to a track listener when track is called with attributes', function() { + var attributes = { + browser_type: 'firefox', + }; + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.activate('testExperiment', 'testUser', attributes); + optlyInstance.track('testEvent', 'testUser', attributes); + var expectedConversionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: 1509489766569, + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var expectedArgument = { + eventKey: 'testEvent', + userId: 'testUser', + attributes: attributes, + eventTags: undefined, + logEvent: expectedConversionEvent, + }; + sinon.assert.calledWith(trackListener, expectedArgument); + }); + + it('should pass the correct arguments to a track listener when track is called with attributes and event tags', function() { + var attributes = { + browser_type: 'firefox', + }; + var eventTags = { + value: 1.234, + non_revenue: 'abc', + }; + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.TRACK, trackListener); + optlyInstance.activate('testExperiment', 'testUser', attributes); + optlyInstance.track('testEvent', 'testUser', attributes, eventTags); + var expectedConversionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '12001', + project_id: '111001', + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: '111095', + timestamp: 1509489766569, + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + key: 'testEvent', + tags: { + non_revenue: 'abc', + value: 1.234, + }, + value: 1.234, + }, + ], + }, + ], + visitor_id: 'testUser', + attributes: [ + { + entity_id: '111094', + key: 'browser_type', + type: 'custom', + value: 'firefox', + }, + ], + }, + ], + revision: '42', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: false, + enrich_decisions: true, + }, + }; + var expectedArgument = { + eventKey: 'testEvent', + userId: 'testUser', + attributes: attributes, + eventTags: eventTags, + logEvent: expectedConversionEvent, + }; + sinon.assert.calledWith(trackListener, expectedArgument); + }); + + describe('Decision Listener', function() { + var decisionListener; + beforeEach(function() { + decisionListener = sinon.spy(); + }); + + describe('activate', function() { + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventProcessor, + notificationCenter, + }); + + optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionListener + ); + }); + + it('should send notification with actual variation key when activate returns variation', function() { + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var variation = optlyInstance.activate('testExperiment', 'testUser'); + assert.strictEqual(variation, 'variation'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.AB_TEST, + userId: 'testUser', + attributes: {}, + decisionInfo: { + experimentKey: 'testExperiment', + variationKey: variation, + }, + }); + }); + + it('should send notification with null variation key when activate returns null', function() { + fakeDecisionResponse = { + result: null, + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var variation = optlyInstance.activate('testExperiment', 'testUser'); + assert.isNull(variation); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.AB_TEST, + userId: 'testUser', + attributes: {}, + decisionInfo: { + experimentKey: 'testExperiment', + variationKey: null, + }, + }); + }); + }); + + describe('getVariation', function() { + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventProcessor, + notificationCenter, + }); + + optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionListener + ); + }); + + it('should send notification with actual variation key when getVariation returns variation', function() { + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var variation = optlyInstance.getVariation('testExperiment', 'testUser'); + assert.strictEqual(variation, 'variation'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.AB_TEST, + userId: 'testUser', + attributes: {}, + decisionInfo: { + experimentKey: 'testExperiment', + variationKey: variation, + }, + }); + }); + + it('should send notification with null variation key when getVariation returns null', function() { + var variation = optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', {}); + assert.isNull(variation); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.AB_TEST, + userId: 'testUser', + attributes: {}, + decisionInfo: { + experimentKey: 'testExperimentWithAudiences', + variationKey: null, + }, + }); + }); + + it('should send notification with variation key and type feature-test when getVariation returns feature experiment variation', function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + }); + + var optly = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventProcessor, + notificationCenter, + }); + + optly.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionListener); + + fakeDecisionResponse = { + result: '594099', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var variation = optly.getVariation('testing_my_feature', 'testUser'); + assert.strictEqual(variation, 'variation2'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_TEST, + userId: 'testUser', + attributes: {}, + decisionInfo: { + experimentKey: 'testing_my_feature', + variationKey: variation, + }, + }); + }); + }); + + describe('feature management', function() { + var sandbox = sinon.sandbox.create(); + + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventDispatcher: eventDispatcher, + eventProcessor, + notificationCenter, + }); + + optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.DECISION, + decisionListener + ); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('isFeatureEnabled', function() { + describe('when the user bucketed into a variation of an experiment of the feature', function() { + var attributes = { test_attribute: 'test_value' }; + + describe('when the variation is toggled ON', function() { + beforeEach(function() { + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap.testing_my_feature; + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return true and send notification', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user1', attributes); + assert.strictEqual(result, true); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'user1', + attributes: attributes, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap.test_shared_feature; + var variation = experiment.variations[1]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return false and send notification', function() { + var result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'user1', + attributes: attributes, + decisionInfo: { + featureKey: 'shared_feature', + featureEnabled: false, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'test_shared_feature', + variationKey: 'control', + }, + }, + }); + }); + }); + }); + + describe('user bucketed into a variation of a rollout of the feature', function() { + describe('when the variation is toggled ON', function() { + beforeEach(function() { + // This experiment is the first audience targeting rule in the rollout of feature 'test_feature' + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap['594031']; + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return true and send notification', function() { + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, true); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + // This experiment is the second audience targeting rule in the rollout of feature 'test_feature' + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap['594037']; + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return false and send notification', function() { + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + sinon.assert.calledWith( + createdLogger.info, + FEATURE_NOT_ENABLED_FOR_USER, + 'test_feature', + 'user1' + ); + + var expectedArguments = { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }; + sinon.assert.calledWith(decisionListener, expectedArguments); + }); + }); + }); + + describe('user not bucketed into an experiment or a rollout', function() { + beforeEach(function() { + var decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return false and send notification', function() { + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'user1', + attributes: {}, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + }); + }); + + describe('feature variable APIs', function() { + describe('bucketed into variation of an experiment with variable values', function() { + describe('when the variation is toggled ON', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + 'testing_my_feature' + ); + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the right value from getFeatureVariable when variable type is boolean and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, true); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'is_button_animated', + variableValue: true, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariable when variable type is double and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 20.25); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'button_width', + variableValue: 20.25, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariable when variable type is integer and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 2); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'num_buttons', + variableValue: 2, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariable when variable type is string and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me NOW'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'button_txt', + variableValue: 'Buy me NOW', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariable when variable type is json and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 1, + text: 'first variation', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'button_info', + variableValue: { + num_buttons: 1, + text: 'first variation', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariableBoolean and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, true); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'is_button_animated', + variableValue: true, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariableDouble and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 20.25); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'button_width', + variableValue: 20.25, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariableInteger and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'num_buttons', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 2); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'num_buttons', + variableValue: 2, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariableString and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableString( + 'test_feature_for_experiment', + 'button_txt', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 'Buy me NOW'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'button_txt', + variableValue: 'Buy me NOW', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getFeatureVariableJSON and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableJSON( + 'test_feature_for_experiment', + 'button_info', + 'user1', + { test_attribute: 'test_value' } + ); + assert.deepEqual(result, { + num_buttons: 1, + text: 'first variation', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableKey: 'button_info', + variableValue: { + num_buttons: 1, + text: 'first variation', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + + it('returns the right value from getAllFeatureVariables and send notification with featureEnabled true', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: true, + button_width: 20.25, + num_buttons: 2, + button_txt: 'Buy me NOW', + button_info: { + num_buttons: 1, + text: 'first variation', + }, + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + variableValues: { + is_button_animated: true, + button_width: 20.25, + num_buttons: 2, + button_txt: 'Buy me NOW', + button_info: { + num_buttons: 1, + text: 'first variation', + }, + }, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + 'testing_my_feature' + ); + var variation = experiment.variations[2]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the default value from getFeatureVariableBoolean and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'is_button_animated', + variableValue: false, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation2', + }, + }, + }); + }); + + it('returns the default value from getFeatureVariableDouble and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 50.55); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_width', + variableValue: 50.55, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation2', + }, + }, + }); + }); + + it('returns the default value from getFeatureVariableInteger and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'num_buttons', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 10); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'num_buttons', + variableValue: 10, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation2', + }, + }, + }); + }); + + it('returns the default value from getFeatureVariableString and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableString( + 'test_feature_for_experiment', + 'button_txt', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 'Buy me'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_txt', + variableValue: 'Buy me', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation2', + }, + }, + }); + }); + + it('returns the default value from getFeatureVariableJSON and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableJSON( + 'test_feature_for_experiment', + 'button_info', + 'user1', + { test_attribute: 'test_value' } + ); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_info', + variableValue: { + num_buttons: 0, + text: 'default value', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation2', + }, + }, + }); + }); + + it('returns the right value from getAllFeatureVariables and send notification with featureEnabled false', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableValues: { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation2', + }, + }, + }); + }); + }); + }); + + describe('bucketed into variation of a rollout with variable values', function() { + describe('when the variation is toggled ON', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + '594031' + ); + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return the right value from getFeatureVariable when variable type is boolean and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, true); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'new_content', + variableValue: true, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariable when variable type is double and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 4.99); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'price', + variableValue: 4.99, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariable when variable type is integer and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 395); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'lasers', + variableValue: 395, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariable when variable type is string and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello audience'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'message', + variableValue: 'Hello audience', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariable when variable type is json and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 2, + message: 'Hello audience', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'message_info', + variableValue: { + count: 2, + message: 'Hello audience', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariableBoolean and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, true); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'new_content', + variableValue: true, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariableDouble and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 4.99); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'price', + variableValue: 4.99, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariableInteger and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 395); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'lasers', + variableValue: 395, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariableString and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableString('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello audience'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'message', + variableValue: 'Hello audience', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the right value from getFeatureVariableJSON and send notification with featureEnabled true', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 2, + message: 'Hello audience', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableKey: 'message_info', + variableValue: { + count: 2, + message: 'Hello audience', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the right value from getAllFeatureVariables and send notification with featureEnabled true', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + new_content: true, + price: 4.99, + lasers: 395, + message: 'Hello audience', + message_info: { + count: 2, + message: 'Hello audience', + }, + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: true, + variableValues: { + new_content: true, + price: 4.99, + lasers: 395, + message: 'Hello audience', + message_info: { + count: 2, + message: 'Hello audience', + }, + }, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + '594037' + ); + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('should return the default value from getFeatureVariable when variable type is boolean and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'new_content', + variableValue: false, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariable when variable type is double and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 14.99); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'price', + variableValue: 14.99, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariable when variable type is integer and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 400); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'lasers', + variableValue: 400, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariable when variable type is string and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'message', + variableValue: 'Hello', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariable when variable type is json and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 1, + message: 'Hello', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'message_info', + variableValue: { count: 1, message: 'Hello' }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariableBoolean and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'new_content', + variableValue: false, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariableDouble and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 14.99); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'price', + variableValue: 14.99, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariableInteger and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 400); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'lasers', + variableValue: 400, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariableString and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableString('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'message', + variableValue: 'Hello', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('should return the default value from getFeatureVariableJSON and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 1, + message: 'Hello', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableKey: 'message_info', + variableValue: { + count: 1, + message: 'Hello', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the default value from getAllFeatureVariables and send notification with featureEnabled false', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + new_content: false, + price: 14.99, + lasers: 400, + message: 'Hello', + message_info: { + count: 1, + message: 'Hello', + }, + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + variableValues: { + new_content: false, + price: 14.99, + lasers: 400, + message: 'Hello', + message_info: { + count: 1, + message: 'Hello', + }, + }, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + }); + }); + + describe('not bucketed into an experiment or a rollout', function() { + beforeEach(function() { + var decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the variable default value from getFeatureVariable when variable type is boolean and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'is_button_animated', + variableValue: false, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariable when variable type is double and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 50.55); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_width', + variableValue: 50.55, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariable when variable type is integer and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 10); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'num_buttons', + variableValue: 10, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariable when variable type is string and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_txt', + variableValue: 'Buy me', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariable when variable type is json and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_info', + variableValue: { + num_buttons: 0, + text: 'default value', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariableBoolean and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'is_button_animated', + variableValue: false, + variableType: FEATURE_VARIABLE_TYPES.BOOLEAN, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariableDouble and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 50.55); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_width', + variableValue: 50.55, + variableType: FEATURE_VARIABLE_TYPES.DOUBLE, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariableInteger and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'num_buttons', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 10); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'num_buttons', + variableValue: 10, + variableType: FEATURE_VARIABLE_TYPES.INTEGER, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariableString and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableString( + 'test_feature_for_experiment', + 'button_txt', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 'Buy me'); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_txt', + variableValue: 'Buy me', + variableType: FEATURE_VARIABLE_TYPES.STRING, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the variable default value from getFeatureVariableJSON and send notification with featureEnabled false', function() { + var result = optlyInstance.getFeatureVariableJSON( + 'test_feature_for_experiment', + 'button_info', + 'user1', + { test_attribute: 'test_value' } + ); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableKey: 'button_info', + variableValue: { + num_buttons: 0, + text: 'default value', + }, + variableType: FEATURE_VARIABLE_TYPES.JSON, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + + it('returns the default value from getAllFeatureVariables and send notification with featureEnabled false', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }); + sinon.assert.calledWith(decisionListener, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: 'user1', + attributes: { test_attribute: 'test_value' }, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: false, + variableValues: { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + }); + }); + }); + }); + }); + }); + + describe('decide APIs', function() { + var optlyInstance; + var bucketStub; + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + var notificationCenter = createNotificationCenter({ logger: createdLogger, }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + + describe('#createUserContext', function() { + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + + bucketStub = sinon.stub(bucketer, 'bucket'); + + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + bucketer.bucket.restore(); + + createdLogger.debug.restore(); + createdLogger.info.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + fns.uuid.restore(); + }); + + it('should create OptimizelyUserContext with provided attributes and userId', function() { + var userId = 'testUser1'; + var attributes = { test_attribute: 'test_value' }; + var user = optlyInstance.createUserContext(userId, attributes); + assert.instanceOf(user, OptimizelyUserContext); + assert.deepEqual(optlyInstance, user.getOptimizely()); + assert.deepEqual(attributes, user.getAttributes()); + assert.deepEqual(userId, user.getUserId()); + }); + + it('should create OptimizelyUserContext when no attributes provided', function() { + var userId = 'testUser2'; + var user = optlyInstance.createUserContext(userId); + assert.instanceOf(user, OptimizelyUserContext); + assert.deepEqual(optlyInstance, user.getOptimizely()); + assert.deepEqual({}, user.getAttributes()); + assert.deepEqual(userId, user.getUserId()); + }); + + it('should create OptimizelyUserContext when input userId is an empty string', function() { + var userId = ''; + var user = optlyInstance.createUserContext(userId); + assert.instanceOf(user, OptimizelyUserContext); + assert.deepEqual(optlyInstance, user.getOptimizely()); + assert.deepEqual({}, user.getAttributes()); + assert.deepEqual(userId, user.getUserId()); + }); + + it('should throw error when input userId is null', function() { + assert.throws(() => { + optlyInstance.createUserContext(null); + }, Error, INVALID_IDENTIFIER); + }); + + it('should throw error when input userId is undefined', function() { + assert.throws(() => { + optlyInstance.createUserContext(undefined); + }, Error, INVALID_IDENTIFIER); + }); + + it('should create multiple instances of OptimizelyUserContext', function() { + var userId1 = 'testUser1'; + var userId2 = 'testUser2'; + var attributes1 = { test_attribute: 'test_value' }; + var user1 = optlyInstance.createUserContext(userId1, attributes1); + var user2 = optlyInstance.createUserContext(userId2); + assert.instanceOf(user1, OptimizelyUserContext); + assert.deepEqual(user1.getOptimizely(), optlyInstance); + assert.deepEqual(user1.getAttributes(), attributes1); + assert.deepEqual(user1.getUserId(), userId1); + assert.instanceOf(user2, OptimizelyUserContext); + assert.deepEqual(user2.getOptimizely(), optlyInstance); + assert.deepEqual(user2.getAttributes(), {}); + assert.deepEqual(user2.getUserId(), userId2); + }); + + it('should call the error handler for invalid user ID and throw', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + assert.throws(() => optlyInstance.createUserContext(1), Error, INVALID_IDENTIFIER); + sinon.assert.calledOnce(errorNotifier.notify); + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); + }); + + it('should call the error handler for invalid attributes and throw', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + assert.throws(() => optlyInstance.createUserContext('user1', 'invalid_attributes'), Error, INVALID_ATTRIBUTES); + sinon.assert.calledOnce(errorNotifier.notify); + // var errorMessage = errorHandler.handleError.lastCall.args[0].message; + // assert.strictEqual(errorMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + // sinon.assert.calledOnce(createdLogger.log); + // var logMessage = buildLogMessageFromArgs(createdLogger.log.args[0]); + // assert.strictEqual(logMessage, sprintf(INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + }); + + describe('#decide', function() { + var userId = 'tester'; + describe('with empty default decide options', function() { + let optlyInstance, notificationCenter, createdLogger; + beforeEach(function() { + + ({ optlyInstance, notificationCenter, createdLogger, eventDispatcher} = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + })); + + + + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + + createdLogger.debug.restore(); + createdLogger.info.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + fns.uuid.restore(); + notificationCenter.sendNotifications.restore(); + }); + + it('should return error decision object when provided flagKey is invalid and do not dispatch an event', function() { + var flagKey = 'invalid_flag_key'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecision = { + variationKey: null, + enabled: false, + variables: {}, + ruleKey: null, + flagKey: flagKey, + userContext: user, + reasons: [sprintf(DECISION_MESSAGES.FLAG_KEY_INVALID, flagKey)], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('should return error decision object when SDK is not ready and do not dispatch an event', function() { + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(null); + var flagKey = 'feature_2'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecision = { + variationKey: null, + enabled: false, + variables: {}, + ruleKey: null, + flagKey: flagKey, + userContext: user, + reasons: [DECISION_MESSAGES.SDK_NOT_READY], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('should make a decision for feature_test and dispatch an event', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance( + { + datafileObj: testData.getTestDecideProjectConfig(), + } + ); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_2'; + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '10367498574', + project_id: '10431130345', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '10417730432', + experiment_id: '10420810910', + variation_id: '10418551353', + metadata: { + flag_key: 'feature_2', + rule_key: 'exp_no_audience', + rule_type: 'feature-test', + variation_key: 'variation_with_traffic', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '10417730432', + timestamp: Math.round(new Date().getTime()), + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'tester', + attributes: [ + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + revision: '241', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: true, + enrich_decisions: true, + }, + }; + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + assert.deepEqual(callArgs[0], expectedImpressionEvent); + sinon.assert.callCount(notificationCenter.sendNotifications, 4); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(3).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_2', + enabled: true, + ruleKey: 'exp_no_audience', + variationKey: 'variation_with_traffic', + variables: { i_42: 42 }, + decisionEventDispatched: true, + reasons: [], + experimentId: '10420810910', + variationId: '10418551353', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should make a decision and do not dispatch an event with DISABLE_DECISION_EVENT passed in decide options', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance( + { + datafileObj: testData.getTestDecideProjectConfig(), + } + ); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_2'; + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey, [OptimizelyDecideOption.DISABLE_DECISION_EVENT]); + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledTwice(notificationCenter.sendNotifications); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(1).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_2', + enabled: true, + ruleKey: 'exp_no_audience', + variationKey: 'variation_with_traffic', + variables: { i_42: 42 }, + decisionEventDispatched: false, + reasons: [], + experimentId: '10420810910', + variationId: '10418551353', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should make a decision with excluded variables and do not dispatch an event with DISABLE_DECISION_EVENT and EXCLUDE_VARIABLES passed in decide options', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance( + { + datafileObj: testData.getTestDecideProjectConfig(), + } + ); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_2'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey, [ + OptimizelyDecideOption.DISABLE_DECISION_EVENT, + OptimizelyDecideOption.EXCLUDE_VARIABLES, + ]); + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledOnce(notificationCenter.sendNotifications); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(0).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_2', + enabled: true, + ruleKey: 'exp_no_audience', + variationKey: 'variation_with_traffic', + variables: {}, + decisionEventDispatched: false, + reasons: [], + experimentId: '10420810910', + variationId: '10418551353', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should make a decision for rollout and dispatch an event when sendFlagDecisions is set to true', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance( + { + datafileObj: testData.getTestDecideProjectConfig(), + } + ); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_1'; + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecision = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables, + ruleKey: '18322080788', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + sinon.assert.callCount(notificationCenter.sendNotifications, 4); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(3).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_1', + enabled: true, + ruleKey: '18322080788', + variationKey: '18257766532', + variables: expectedVariables, + decisionEventDispatched: true, + reasons: [], + experimentId: '18322080788', + variationId: '18257766532', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should make a decision for rollout and do not dispatch an event when sendFlagDecisions is set to false', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance( + { + datafileObj: testData.getTestDecideProjectConfig(), + } + ); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = false; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var flagKey = 'feature_1'; + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecision = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables, + ruleKey: '18322080788', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledTwice(notificationCenter.sendNotifications); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(1).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_1', + enabled: true, + ruleKey: '18322080788', + variationKey: '18257766532', + variables: expectedVariables, + decisionEventDispatched: false, + reasons: [], + experimentId: '18322080788', + variationId: '18257766532', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should make a decision when variation is null and dispatch an event', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance( + { + datafileObj: testData.getTestDecideProjectConfig(), + } + ); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_3'; + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecision = { + variationKey: null, + enabled: false, + variables: expectedVariables, + ruleKey: null, + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + sinon.assert.callCount(notificationCenter.sendNotifications, 4); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(3).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_3', + enabled: false, + ruleKey: null, + variationKey: null, + variables: expectedVariables, + decisionEventDispatched: true, + reasons: [], + experimentId: null, + variationId: null, + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + }); + + describe('with EXCLUDE_VARIABLES flag in default decide options', function() { + it('should exclude variables in decision object and dispatch an event', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + defaultDecideOptions: [OptimizelyDecideOption.EXCLUDE_VARIABLES], + }); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_2'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecisionObj = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecisionObj); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + sinon.assert.calledThrice(notificationCenter.sendNotifications); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(2).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_2', + enabled: true, + ruleKey: 'exp_no_audience', + variationKey: 'variation_with_traffic', + variables: {}, + decisionEventDispatched: true, + reasons: [], + experimentId: "10420810910", + variationId: "10418551353", + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should exclude variables in decision object and do not dispatch an event when DISABLE_DECISION_EVENT is passed in decide options', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + defaultDecideOptions: [OptimizelyDecideOption.EXCLUDE_VARIABLES], + }) + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var flagKey = 'feature_2'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey, [OptimizelyDecideOption.DISABLE_DECISION_EVENT]); + var expectedDecisionObj = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecisionObj); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledOnce(notificationCenter.sendNotifications); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(0).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_2', + enabled: true, + ruleKey: 'exp_no_audience', + variationKey: 'variation_with_traffic', + variables: {}, + decisionEventDispatched: false, + reasons: [], + experimentId: '10420810910', + variationId: '10418551353', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + }); + + describe('with DISABLE_DECISION_EVENT flag in default decide options', function() { + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [OptimizelyDecideOption.DISABLE_DECISION_EVENT], + notificationCenter, + eventProcessor, + }); + + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + }); + + afterEach(function() { + optlyInstance.notificationCenter.sendNotifications.restore(); + }); + + it('should make a decision and do not dispatch an event', function() { + var flagKey = 'feature_2'; + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + var expectedDecisionObj = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(decision, expectedDecisionObj); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledTwice(optlyInstance.notificationCenter.sendNotifications); + var notificationCallArgs = optlyInstance.notificationCenter.sendNotifications.getCall(1).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: 'feature_2', + enabled: true, + ruleKey: 'exp_no_audience', + variationKey: 'variation_with_traffic', + variables: expectedVariables, + decisionEventDispatched: false, + reasons: [], + experimentId: '10420810910', + variationId: '10418551353', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + }); + + describe('with INCLUDE_REASONS flag in default decide options', function() { + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [OptimizelyDecideOption.INCLUDE_REASONS], + notificationCenter, + eventProcessor, + }); + + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + optlyInstance.notificationCenter.sendNotifications.restore(); + }); + + it('should include reason when experiment is not running', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].status = 'NotRunning'; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var flagKey = 'feature_1'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(EXPERIMENT_NOT_RUNNING, 'exp_with_audience') + ); + }); + + it('should include reason when returning stored variation from user profile', function() { + var flagKey = 'feature_2'; + var variationKey2 = 'variation_no_traffic'; + var variationId2 = '10418510624'; + var experimentKey = 'exp_no_audience'; + var mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { + // "exp_no_audience" + variation_id: variationId2, + }, + }, + }), + save: sinon.stub(), + }; + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + var optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [OptimizelyDecideOption.INCLUDE_REASONS], + notificationCenter, + eventProcessor, + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId, + }); + var decision = optlyInstanceWithUserProfile.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(RETURNING_STORED_VARIATION, variationKey2, experimentKey, userId) + ); + }); + + it('should include reason when user is forced in variation', function() { + var flagKey = 'feature_1'; + var variationKey = 'b'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].forcedVariations[userId] = variationKey; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_FORCED_IN_VARIATION, userId, variationKey) + ); + }); + + it('should include reason when user has forced variation', function() { + var flagKey = 'feature_1'; + var variationKey = 'b'; + var experimentKey = 'exp_with_audience'; + optlyInstance.decisionService.forcedVariationMap[userId] = { '10390977673': variationKey }; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.variationIdMap[variationKey] = { key: variationKey }; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_HAS_FORCED_VARIATION, variationKey, experimentKey, userId) + ); + }); + + it('should include reason when invalid forced variation found', function() { + var flagKey = 'feature_1'; + var variationKey = 'invalid-key'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].forcedVariations[userId] = variationKey; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + + expect(decision.reasons).to.include( + sprintf(FORCED_BUCKETING_FAILED, variationKey, userId) + ); + }); + + it('should include reason when user meets conditions for targeting rule', function() { + var flagKey = 'feature_1'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + user.setAttribute('country', 'US'); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, userId, '1') + ); + }); + + it('should include reason when user does not meet conditions for targeting rule', function() { + var flagKey = 'feature_1'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + user.setAttribute('country', 'CA'); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, userId, '1') + ); + }); + + it('should include reason when user is bucketed into targeting rule', function() { + var flagKey = 'feature_1'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + user.setAttribute('country', 'US'); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_IN_ROLLOUT, userId, flagKey) + ); + }); + + it('should include reason when user is bucketed into everyone targeting rule', function() { + var flagKey = 'feature_1'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + user.setAttribute('country', 'KO'); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, userId, 'Everyone Else') + ); + }); + + it('should include reason when user is not bucketed into targeting rule', function() { + var flagKey = 'feature_1'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + user.setAttribute('browser', 'safari'); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_NOT_BUCKETED_INTO_TARGETING_RULE, userId, '2') + ); + }); + + it('should include reason when user is bucketed into variation of experiment', function() { + var flagKey = 'feature_2'; + var experimentKey = 'exp_no_audience'; + var variationKey = 'variation_with_traffic'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_HAS_VARIATION, userId, variationKey, experimentKey) + ); + }); + + it('should include reason when user is not bucketed into variation of experiment', function() { + var flagKey = 'feature_2'; + var experimentKey = 'exp_no_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[1].trafficAllocation = []; + newConfig.experiments[1].trafficAllocation.push({ endOfRange: 0, entityId: 'any' }); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attributes: { age: 25 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_HAS_NO_VARIATION, userId, experimentKey) + ); + }); + + it('should include reason when user is bucketed into experiment in group', function() { + var flagKey = 'feature_3'; + var experimentId = '10390965532'; + var experimentKey = 'group_exp_1'; + var groupId = '13142870430'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.featureFlags[2].experimentIds.push(experimentId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + + expect(decision.reasons).to.include( + sprintf(USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, userId, experimentKey, groupId) + ); + }); + + it('should include reason when user is not attached to any experiment', function() { + var flagKey = 'feature_3'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.groups[0].trafficAllocation = []; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(FEATURE_HAS_NO_EXPERIMENTS, flagKey) + ); + }); + + it('should include reason when user is not in experiment', function() { + var flagKey = 'feature_1'; + var experimentKey = 'exp_with_audience'; + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf(USER_NOT_IN_EXPERIMENT, userId, experimentKey) + ); + }); + + it('should include reason when condition does not match the audience', function() { + var flagKey = 'feature_1'; + var audienceId = 'invalid_id'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when evaluating attribute with invalid type', function() { + var flagKey = 'feature_1'; + var audienceId = '13389130056'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attirutes: { country: 25 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when attribute value is out of range', function() { + var flagKey = 'feature_1'; + var audienceId = 'age_18'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attirutes: { age: 10000 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when provided invalid type user attribute', function() { + var flagKey = 'feature_1'; + var audienceId = 'invalid_type'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attirutes: { age: 25 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when audience id is invalid_type', function() { + var flagKey = 'feature_1'; + var audienceId = 'invalid_type'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attirutes: { age: 25 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when audience id is invalid_match', function() { + var flagKey = 'feature_1'; + var audienceId = 'invalid_match'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attirutes: { age: 25 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when audience id is nil_value', function() { + var flagKey = 'feature_1'; + var audienceId = 'nil_value'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + attirutes: { age: 25 }, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + + it('should include reason when user attributes is missing', function() { + var flagKey = 'feature_1'; + var audienceId = 'age_18'; + var experimentKey = 'exp_with_audience'; + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.experiments[0].audienceIds = []; + newConfig.experiments[0].audienceIds.push(audienceId); + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var user = new OptimizelyUserContext({ + optimizely: optlyInstance, + userId, + }); + var decision = optlyInstance.decide(user, flagKey); + expect(decision.reasons).to.include( + sprintf( + AUDIENCE_EVALUATION_RESULT_COMBINED, + 'experiment', + experimentKey, + 'FALSE' + ) + ); + }); + }); + + describe('when user profile service provided', function() { + var mockUserProfileServiceInstance; + var optlyInstanceWithUserProfile; + it('should bucket if there was no previously bucketed variation and save bucketing decision to the user profile', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId1 = '10418551353'; + var variationKey1 = 'variation_with_traffic'; + mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: {}, + }), + save: sinon.stub(), + }; + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId, + }); + var decision1 = optlyInstanceWithUserProfile.decide(user, flagKey); + // should return variationId1 as no stored variation exists + assert.equal(variationKey1, decision1.variationKey); + // also should call mockUserProfileServiceInstance.save to save bucketing decision + sinon.assert.calledOnce(mockUserProfileServiceInstance.save); + }); + + describe('with IGNORE_USER_PROFILE_SERVICE flag in decide options', function() { + it('should bypass user profile service', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId1 = '10418551353'; + var variationId2 = '10418510624'; + var variationKey1 = 'variation_with_traffic'; + var variationKey2 = 'variation_no_traffic'; + mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { + // 'exp_no_audience' + variation_id: variationId2, + }, + }, + }), + save: sinon.stub(), + }; + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId, + }); + var decision1 = optlyInstanceWithUserProfile.decide(user, flagKey); + // should return variationId2 set by UPS + assert.equal(variationKey2, decision1.variationKey); + var decision2 = optlyInstanceWithUserProfile.decide(user, flagKey, [ + OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE, + ]); + // should ignore variationId2 set by UPS and return variationId1 + assert.equal(variationKey1, decision2.variationKey); + // also should not save either + sinon.assert.notCalled(mockUserProfileServiceInstance.save); + }); + }); + + describe('with IGNORE_USER_PROFILE_SERVICE flag in default decide options', function() { + it('should bypass user profile service', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId2 = '10418510624'; + var variationKey1 = 'variation_with_traffic'; + mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { + // 'exp_no_audience' + variation_id: variationId2, + }, + }, + }), + save: sinon.stub(), + }; + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }); + + optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE], + notificationCenter, + eventProcessor, + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId, + }); + var decision = optlyInstanceWithUserProfile.decide(user, flagKey); + // should ignore variationId2 set by UPS and return variationId1 + assert.equal(variationKey1, decision.variationKey); + // also should not save either + sinon.assert.notCalled(mockUserProfileServiceInstance.save); + }); + }); + }); + }); + + + describe('#decideForKeys', function() { + var userId = 'tester'; + it('should return decision results map with single flag key provided for feature_test and dispatch an event', function() { + var flagKey = 'feature_2'; + const { optlyInstance, eventDispatcher } = getOptlyInstance({ datafileObj: testData.getTestDecideProjectConfig() }); + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + var user = optlyInstance.createUserContext(userId); + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + + var decisionsMap = optlyInstance.decideForKeys(user, [flagKey]); + var decision = decisionsMap[flagKey]; + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, 1); + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + sinon.assert.callCount(optlyInstance.notificationCenter.sendNotifications, 4); + var notificationCallArgs = optlyInstance.notificationCenter.sendNotifications.getCall(3).args; + var decisionEventDispatched = notificationCallArgs[1].decisionInfo.decisionEventDispatched; + assert.deepEqual(decisionEventDispatched, true); + }); + + it('should return decision results map with two flag keys provided and dispatch events', function() { + var flagKeysArray = ['feature_1', 'feature_2']; + const { optlyInstance, eventDispatcher } = getOptlyInstance({ datafileObj: testData.getTestDecideProjectConfig() }); + var user = optlyInstance.createUserContext(userId); + + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKeysArray[0], userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKeysArray[1], userId); + var decisionsMap = optlyInstance.decideForKeys(user, flagKeysArray); + var decision1 = decisionsMap[flagKeysArray[0]]; + var decision2 = decisionsMap[flagKeysArray[1]]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKeysArray[0], + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKeysArray[1], + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledTwice(eventDispatcher.dispatchEvent); + }); + + it('should return decision results map with only enabled flags when ENABLED_FLAGS_ONLY flag is passed in and dispatch events', function() { + var flagKey1 = 'feature_2'; + var flagKey2 = 'feature_3'; + const { optlyInstance, eventDispatcher } = getOptlyInstance({ datafileObj: testData.getTestDecideProjectConfig() }); + var user = optlyInstance.createUserContext(userId, { gender: 'female' }); + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey1, userId); + var decisionsMap = optlyInstance.decideForKeys( + user, + [flagKey1, flagKey2], + [OptimizelyDecideOption.ENABLED_FLAGS_ONLY] + ); + var decision = decisionsMap[flagKey1]; + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey1, + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, 1); + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledTwice(eventDispatcher.dispatchEvent); + }); + describe('UPS Batching', function() { + var userProfileServiceInstance = { + lookup: function() {}, + save: function() {}, + }; + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }), + userProfileService: userProfileServiceInstance, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [], + notificationCenter, + eventProcessor, + }); + + sinon.stub(optlyInstance.decisionService.userProfileService, 'lookup') + sinon.stub(optlyInstance.decisionService.userProfileService, 'save') + // + }); + + it('Should call UPS methods only once', function() { + var flagKeysArray = ['feature_1', 'feature_2']; + var user = optlyInstance.createUserContext(userId); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKeysArray[0], userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKeysArray[1], userId); + optlyInstance.decisionService.userProfileService.save.resetHistory(); + optlyInstance.decisionService.userProfileService.lookup.resetHistory(); + var decisionsMap = optlyInstance.decideForKeys(user, flagKeysArray); + var decision1 = decisionsMap[flagKeysArray[0]]; + var decision2 = decisionsMap[flagKeysArray[1]]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKeysArray[0], + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKeysArray[1], + userContext: user, + reasons: [], + }; + var userProfile = { + user_id: userId, + experiment_bucket_map: { + '10420810910': { // ruleKey from expectedDecision1 + variation_id: '10418551353' // variationKey from expectedDecision1 + } + } + }; + + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + // UPS batch assertion + sinon.assert.calledOnce(optlyInstance.decisionService.userProfileService.lookup); + sinon.assert.calledOnce(optlyInstance.decisionService.userProfileService.save); + + // UPS save assertion + sinon.assert.calledWithExactly(optlyInstance.decisionService.userProfileService.save, userProfile); + }); + }) + + }); + + describe('#decideAll', function() { + var userId = 'tester'; + describe('with empty default decide options', function() { + + it('should return decision results map with all flag keys provided and dispatch events', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance({ datafileObj: testData.getTestDecideProjectConfig() }); + var configObj = optlyInstance.projectConfigManager.getConfig(); + var allFlagKeysArray = Object.keys(configObj.featureKeyMap); + var user = optlyInstance.createUserContext(userId); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[0], userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[1], userId); + var expectedVariables3 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[2], userId); + var decisionsMap = user.decideAll(allFlagKeysArray); + var decision1 = decisionsMap[allFlagKeysArray[0]]; + var decision2 = decisionsMap[allFlagKeysArray[1]]; + var decision3 = decisionsMap[allFlagKeysArray[2]]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: allFlagKeysArray[0], + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: allFlagKeysArray[1], + userContext: user, + reasons: [], + }; + var expectedDecision3 = { + variationKey: null, + enabled: false, + variables: expectedVariables3, + ruleKey: null, + flagKey: allFlagKeysArray[2], + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, allFlagKeysArray.length); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + assert.deepEqual(decision3, expectedDecision3); + sinon.assert.calledThrice(eventDispatcher.dispatchEvent); + }); + + it('should return decision results map with only enabled flags when ENABLED_FLAGS_ONLY flag is passed in and dispatch events', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + const { optlyInstance, eventDispatcher } = getOptlyInstance({ datafileObj: testData.getTestDecideProjectConfig() }); + var user = optlyInstance.createUserContext(userId, { gender: 'female' }); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId); + var decisionsMap = optlyInstance.decideAll(user, [OptimizelyDecideOption.ENABLED_FLAGS_ONLY]); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledThrice(eventDispatcher.dispatchEvent); + }); + }); + + describe('with ENABLED_FLAGS_ONLY flag in default decide options', function() { + it('should return decision results map with only enabled flags and dispatch events', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + defaultDecideOptions: [OptimizelyDecideOption.ENABLED_FLAGS_ONLY] + }); + var user = optlyInstance.createUserContext(userId, { gender: 'female' }); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId); + var decisionsMap = optlyInstance.decideAll(user); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledThrice(eventDispatcher.dispatchEvent); + }); + + it('should return decision results map with only enabled flags and excluded variables when EXCLUDE_VARIABLES_FLAG is passed in', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + defaultDecideOptions: [OptimizelyDecideOption.ENABLED_FLAGS_ONLY] + }); + + var user = optlyInstance.createUserContext(userId, { gender: 'female' }); + var decisionsMap = optlyInstance.decideAll(user, [OptimizelyDecideOption.EXCLUDE_VARIABLES]); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + }; + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledThrice(eventDispatcher.dispatchEvent); + }); + }); + + describe('UPS batching', function() { + beforeEach(function() { + var userProfileServiceInstance = { + lookup: function() {}, + save: function() {}, + }; + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), + }), + userProfileService: userProfileServiceInstance, + + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [OptimizelyDecideOption.ENABLED_FLAGS_ONLY], + eventProcessor, + notificationCenter, + }); + + sinon.stub(optlyInstance.decisionService.userProfileService, 'lookup') + sinon.stub(optlyInstance.decisionService.userProfileService, 'save') + }); + + it('should call UPS methods only once', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var user = optlyInstance.createUserContext(userId, { gender: 'female' }); + var decisionsMap = optlyInstance.decideAll(user, [OptimizelyDecideOption.EXCLUDE_VARIABLES]); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + }; + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + }; + + // Decision assertion + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + + // UPS batch assertion + sinon.assert.calledOnce(optlyInstance.decisionService.userProfileService.lookup); + sinon.assert.calledOnce(optlyInstance.decisionService.userProfileService.save); + }) + }); + }); + }); + + //tests separated out from APIs because of mock bucketing + describe('getVariationBucketingIdAttribute', function() { + var optlyInstance; + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + var notificationCenter = createNotificationCenter({ logger: createdLogger, }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + + var userAttributes = { + browser_type: 'firefox', + }; + var userAttributesWithBucketingId = { + browser_type: 'firefox', + $opt_bucketing_id: '123456789', + }; + + it('confirm that a valid variation is bucketed without the bucketing ID', function() { + assert.strictEqual( + 'controlWithAudience', + optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', userAttributes) + ); + }); + + it('confirm that an invalid audience returns null', function() { + assert.strictEqual(null, optlyInstance.getVariation('testExperimentWithAudiences', 'testUser')); + }); + + it('confirm that a valid variation is bucketed with the bucketing ID', function() { + assert.strictEqual( + 'variationWithAudience', + optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', userAttributesWithBucketingId) + ); + }); + + it('confirm that invalid experiment with the bucketing ID returns null', function() { + assert.strictEqual( + null, + optlyInstance.getVariation('invalidExperimentKey', 'testUser', userAttributesWithBucketingId) + ); + }); + }); + + describe('feature management', function() { + var sandbox = sinon.sandbox.create(); + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + var optlyInstance; + var fakeDecisionResponse; + var notificationCenter = createNotificationCenter({ logger: createdLogger, }); + var eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; + + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + + sandbox.stub(eventDispatcher, 'dispatchEvent'); + sandbox.stub(createdLogger, 'log'); + sandbox.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); + sandbox.stub(fns, 'currentTimestamp').returns(1509489766569); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#isFeatureEnabled', function() { + it('returns false, and does not dispatch an impression event, for an invalid feature key', function() { + var result = optlyInstance.isFeatureEnabled('thisIsDefinitelyNotAFeatureKey', 'user1'); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('returns false if the instance is invalid', function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager(), + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + eventProcessor, + notificationCenter, + }); + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user1'); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Optimizely object is not valid. Failing isFeatureEnabled.' + // ); + }); + + describe('when the user bucketed into a variation of an experiment with the feature', function() { + var attributes = { test_attribute: 'test_value' }; + + describe('when the variation is toggled ON', function() { + beforeEach(function() { + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap.testing_my_feature; + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }); + + it('returns true and dispatches an impression event', function() { + var user = optlyInstance.createUserContext('user1', attributes); + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user1', attributes); + assert.strictEqual(result, true); + sinon.assert.calledOnce(optlyInstance.decisionService.getVariationForFeature); + var feature = optlyInstance.projectConfigManager.getConfig().featureKeyMap.test_feature_for_experiment; + sinon.assert.calledWithExactly( + optlyInstance.decisionService.getVariationForFeature, + optlyInstance.projectConfigManager.getConfig(), + feature, + user + ); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '572018', + project_id: '594001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '594093', + experiment_id: '594098', + variation_id: '594096', + metadata: { + flag_key: 'test_feature_for_experiment', + rule_key: 'testing_my_feature', + rule_type: 'feature-test', + variation_key: 'variation', + enabled: true, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '594093', + timestamp: 1509489766569, + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'user1', + attributes: [ + { + entity_id: '594014', + key: 'test_attribute', + type: 'custom', + value: 'test_value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + revision: '35', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: true, + enrich_decisions: true, + }, + }; + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + assert.deepEqual(callArgs[0], expectedImpressionEvent); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature test_feature_for_experiment is enabled for user user1.' + // ); + }); + + it('returns false and does not dispatch an impression event when feature key is null', function() { + var result = optlyInstance.isFeatureEnabled(null, 'user1', attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided feature_key is in an invalid format.' + // ); + }); + + it('returns false when user id is null', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', null, attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns false when feature key and user id are null', function() { + var result = optlyInstance.isFeatureEnabled(null, null, attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns false when feature key is undefined', function() { + var result = optlyInstance.isFeatureEnabled(undefined, 'user1', attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided feature_key is in an invalid format.' + // ); + }); + + it('returns false when user id is undefined', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', undefined, attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns false when feature key and user id are undefined', function() { + var result = optlyInstance.isFeatureEnabled(undefined, undefined, attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('returns false when no arguments are provided', function() { + var result = optlyInstance.isFeatureEnabled(); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns false when user id is an object', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', {}, attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns false when user id is a number', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 72, attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns false when feature key is an array', function() { + var result = optlyInstance.isFeatureEnabled(['a', 'feature'], 'user1', attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // sinon.assert.calledWithExactly( + // createdLogger.log, + // LOG_LEVEL.ERROR, + // 'OPTIMIZELY: Provided feature_key is in an invalid format.' + // ); + }); + + it('returns true when user id is an empty string', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', '', attributes); + assert.strictEqual(result, true); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + }); + + it('returns false when feature key is an empty string', function() { + var result = optlyInstance.isFeatureEnabled('', 'user1', attributes); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('returns false when a feature key is provided, but a user id is not', function() { + var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment'); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + }); + + describe('when the variation is toggled OFF', function() { + var result; + beforeEach(function() { + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap.test_shared_feature; + var variation = experiment.variations[1]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }); + + it('should return false', function() { + assert.strictEqual(result, false); + sinon.assert.calledOnce(optlyInstance.decisionService.getVariationForFeature); + var feature = optlyInstance.projectConfigManager.getConfig().featureKeyMap.shared_feature; + var user = optlyInstance.createUserContext('user1', attributes); + sinon.assert.calledWithExactly( + optlyInstance.decisionService.getVariationForFeature, + optlyInstance.projectConfigManager.getConfig(), + feature, + user + ); + }); + + it('should dispatch an impression event', function() { + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '572018', + project_id: '594001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: '599023', + experiment_id: '599028', + variation_id: '599027', + metadata: { + flag_key: 'shared_feature', + rule_key: 'test_shared_feature', + rule_type: 'feature-test', + variation_key: 'control', + enabled: false, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: '599023', + timestamp: 1509489766569, + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'user1', + attributes: [ + { + entity_id: '594014', + key: 'test_attribute', + type: 'custom', + value: 'test_value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + revision: '35', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: true, + enrich_decisions: true, + }, + }; + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + assert.deepEqual(callArgs[0], expectedImpressionEvent); + }); + }); + + describe('when the variation is missing the toggle', function() { + beforeEach(function() { + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap.test_shared_feature; + var variation = experiment.variations[0]; + delete variation['featureEnabled']; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + + it('should return false', function() { + var result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); + var user = optlyInstance.createUserContext('user1', attributes); + assert.strictEqual(result, false); + sinon.assert.calledOnce(optlyInstance.decisionService.getVariationForFeature); + var feature = optlyInstance.projectConfigManager.getConfig().featureKeyMap.shared_feature; + sinon.assert.calledWithExactly( + optlyInstance.decisionService.getVariationForFeature, + optlyInstance.projectConfigManager.getConfig(), + feature, + user + ); + }); + }); + }); + + describe('user bucketed into a variation of a rollout of the feature', function() { + describe('when the variation is toggled ON', function() { + beforeEach(function() { + // This experiment is the first audience targeting rule in the rollout of feature 'test_feature' + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap['594031']; + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns true and does not dispatch an event', function() { + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, true); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature test_feature is enabled for user user1.' + // ); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + // This experiment is the second audience targeting rule in the rollout of feature 'test_feature' + var experiment = optlyInstance.projectConfigManager.getConfig().experimentKeyMap['594037']; + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns false', function() { + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature test_feature is not enabled for user user1.' + // ); + }); + }); + }); + + describe('user not bucketed into an experiment or a rollout', function() { + beforeEach(function() { + var decisionObj = { + experiment: null, + variation: null, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns false and does not dispatch an event when sendFlagDecisions is not defined', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = undefined; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature test_feature is not enabled for user user1.' + // ); + }); + + it('returns false and does not dispatch an event when sendFlagDecisions is set to false', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = false; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); + assert.strictEqual(result, false); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature test_feature is not enabled for user user1.' + // ); + }); + + it('returns false and dispatch an event when sendFlagDecisions is set to true', function() { + var newConfig = optlyInstance.projectConfigManager.getConfig(); + newConfig.sendFlagDecisions = true; + optlyInstance.projectConfigManager.getConfig = sinon.stub().returns(newConfig); + var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); + assert.strictEqual(result, false); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var expectedImpressionEvent = { + httpVerb: 'POST', + url: 'https://logx.optimizely.com/v1/events', + params: { + account_id: '572018', + project_id: '594001', + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: null, + experiment_id: '', + variation_id: '', + metadata: { + flag_key: 'test_feature', + rule_key: '', + rule_type: 'rollout', + variation_key: '', + enabled: false, + cmab_uuid: undefined, + }, + }, + ], + events: [ + { + entity_id: null, + timestamp: 1509489766569, + key: 'campaign_activated', + uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + }, + ], + }, + ], + visitor_id: 'user1', + attributes: [ + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + revision: '35', + client_name: 'node-sdk', + client_version: enums.CLIENT_VERSION, + anonymize_ip: true, + enrich_decisions: true, + }, + }; + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + assert.deepEqual(callArgs[0], expectedImpressionEvent); + }); + }); + }); + + describe('#getEnabledFeatures', function() { + beforeEach(function() { + sandbox.stub(optlyInstance, 'isFeatureEnabled').callsFake(function(featureKey) { + return featureKey === 'test_feature' || featureKey === 'test_feature_for_experiment'; + }); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + + it('returns an empty array if the instance is invalid', function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager(), + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + eventProcessor, + notificationCenter, + }); + var result = optlyInstance.getEnabledFeatures('user1', { test_attribute: 'test_value' }); + assert.deepEqual(result, []); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Optimizely object is not valid. Failing getEnabledFeatures.' + // ); + }); + + it('returns only enabled features for the specified user and attributes', function() { + var attributes = { test_attribute: 'test_value' }; + var result = optlyInstance.getEnabledFeatures('user1', attributes); + assert.strictEqual(result.length, 2); + assert.isAbove(result.indexOf('test_feature'), -1); + assert.isAbove(result.indexOf('test_feature_for_experiment'), -1); + sinon.assert.callCount(optlyInstance.isFeatureEnabled, 9); + sinon.assert.calledWithExactly(optlyInstance.isFeatureEnabled, 'test_feature', 'user1', attributes); + sinon.assert.calledWithExactly(optlyInstance.isFeatureEnabled, 'test_feature_2', 'user1', attributes); + sinon.assert.calledWithExactly( + optlyInstance.isFeatureEnabled, + 'test_feature_for_experiment', + 'user1', + attributes + ); + sinon.assert.calledWithExactly(optlyInstance.isFeatureEnabled, 'feature_with_group', 'user1', attributes); + sinon.assert.calledWithExactly(optlyInstance.isFeatureEnabled, 'shared_feature', 'user1', attributes); + sinon.assert.calledWithExactly(optlyInstance.isFeatureEnabled, 'unused_flag', 'user1', attributes); + sinon.assert.calledWithExactly(optlyInstance.isFeatureEnabled, 'feature_exp_no_traffic', 'user1', attributes); + }); + + it('return features that are enabled for the user and send notification for every feature', function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventProcessor, + notificationCenter, + }); + + var decisionListener = sinon.spy(); + var attributes = { test_attribute: 'test_value' }; + optlyInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.DECISION, decisionListener); + var result = optlyInstance.getEnabledFeatures('test_user', attributes); + assert.strictEqual(result.length, 5); + assert.deepEqual(result, [ + 'test_feature_2', + 'test_feature_for_experiment', + 'shared_feature', + 'test_feature_in_exclusion_group', + 'test_feature_in_multiple_experiments', + ]); + + sinon.assert.calledWithExactly(decisionListener.getCall(0), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'test_feature', + featureEnabled: false, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + sinon.assert.calledWithExactly(decisionListener.getCall(1), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'test_feature_2', + featureEnabled: true, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + sinon.assert.calledWithExactly(decisionListener.getCall(2), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'test_feature_for_experiment', + featureEnabled: true, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'testing_my_feature', + variationKey: 'variation', + }, + }, + }); + sinon.assert.calledWithExactly(decisionListener.getCall(3), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'feature_with_group', + featureEnabled: false, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + sinon.assert.calledWithExactly(decisionListener.getCall(4), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'shared_feature', + featureEnabled: true, + source: DECISION_SOURCES.FEATURE_TEST, + sourceInfo: { + experimentKey: 'test_shared_feature', + variationKey: 'treatment', + }, + }, + }); + sinon.assert.calledWithExactly(decisionListener.getCall(5), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'unused_flag', + featureEnabled: false, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + sinon.assert.calledWithExactly(decisionListener.getCall(6), { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: 'test_user', + attributes: attributes, + decisionInfo: { + featureKey: 'feature_exp_no_traffic', + featureEnabled: false, + source: DECISION_SOURCES.ROLLOUT, + sourceInfo: {}, + }, + }); + }); + }); + + describe('feature variable APIs', function() { + describe('bucketed into variation in an experiment with variable values', function() { + describe('when the variation is toggled ON', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + 'testing_my_feature' + ); + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the right value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, true); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "true" for variable "is_button_animated" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 20.25); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "20.25" for variable "button_width" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 2); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "2" for variable "num_buttons" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me NOW'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "Buy me NOW" for variable "button_txt" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 1, + text: 'first variation', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "{ "num_buttons": 1, "text": "first variation"}" for variable "button_info" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, true); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "true" for variable "is_button_animated" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 20.25); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "20.25" for variable "button_width" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'num_buttons', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 2); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "2" for variable "num_buttons" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me NOW'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "Buy me NOW" for variable "button_txt" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 1, + text: 'first variation', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "{ "num_buttons": 1, "text": "first variation"}" for variable "button_info" of feature flag "test_feature_for_experiment"' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: true, + button_width: 20.25, + num_buttons: 2, + button_txt: 'Buy me NOW', + button_info: { + num_buttons: 1, + text: 'first variation', + }, + }); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // '2', + // 'num_buttons', + // 'test_feature_for_experiment' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // 'true', + // 'is_button_animated', + // 'test_feature_for_experiment' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // 'Buy me NOW', + // 'button_txt', + // 'test_feature_for_experiment' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // '20.25', + // 'button_width', + // 'test_feature_for_experiment' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // '{ "num_buttons": 1, "text": "first variation"}', + // 'button_info', + // 'test_feature_for_experiment' + // ); + }); + + describe('when the variable is not used in the variation', function() { + beforeEach(function() { + sandbox.stub(projectConfig, 'getVariableValueForVariation').returns(null); + }); + + it('returns the variable default value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "is_button_animated" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 50.55); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "button_width" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 10); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "num_buttons" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "button_txt" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "button_info" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "is_button_animated" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 50.55); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "button_width" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'num_buttons', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 10); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "num_buttons" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString( + 'test_feature_for_experiment', + 'button_txt', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 'Buy me'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "button_txt" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "button_info" is not used in variation "variation". Returning default value.' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }); + // sinon.assert.calledWith( + // createdLogger.info, + // // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'num_buttons', + // 'variation' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'is_button_animated', + // 'variation' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'button_txt', + // 'variation' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'button_width', + // 'variation' + // ); + // sinon.assert.calledWith( + // createdLogger.log, + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'button_info', + // 'variation' + // ); + }); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + 'testing_my_feature' + ); + var variation = experiment.variations[2]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.FEATURE_TEST, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the variable default value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "false".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 50.55); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "50.55".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 10); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "10".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "Buy me".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "{ "num_buttons": 0, "text": "default value"}".' + // ); + }); + + it('returns the variable default value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "false".' + // ); + }); + + it('returns the variable default value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'button_width', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 50.55); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "50.55".' + // ); + }); + + it('returns the variable default value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'num_buttons', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, 10); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "10".' + // ); + }); + + it('returns the variable default value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "Buy me".' + // ); + }); + + it('returns the variable default value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature_for_experiment" is not enabled for user user1. Returning the default variable value "{ "num_buttons": 0, "text": "default value"}".' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }); + // assert.deepEqual(createdLogger.log.args, [ + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature_for_experiment', + // 'user1', + // '10', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature_for_experiment', + // 'user1', + // 'false', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature_for_experiment', + // 'user1', + // 'Buy me', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature_for_experiment', + // 'user1', + // '50.55', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature_for_experiment', + // 'user1', + // '{ "num_buttons": 0, "text": "default value"}', + // ], + // ]); + }); + }); + }); + + describe('bucketed into variation of a rollout with variable values', function() { + describe('when the variation is toggled ON', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + '594031' + ); + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the right value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, true); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "true" for variable "new_content" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 4.99); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "4.99" for variable "price" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 395); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "395" for variable "lasers" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello audience'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "Hello audience" for variable "message" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 2, + message: 'Hello audience', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "{ "count": 2, "message": "Hello audience" }" for variable "message_info" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, true); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "true" for variable "new_content" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 4.99); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "4.99" for variable "price" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 395); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "395" for variable "lasers" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello audience'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "Hello audience" for variable "message" of feature flag "test_feature"' + // ); + }); + + it('returns the right value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 2, + message: 'Hello audience', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Got variable value "{ "count": 2, "message": "Hello audience" }" for variable "message_info" of feature flag "test_feature"' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + new_content: true, + price: 4.99, + lasers: 395, + message: 'Hello audience', + message_info: { + count: 2, + message: 'Hello audience', + }, + }); + // assert.deepEqual(createdLogger.log.args, [ + // [ + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // 'true', + // 'new_content', + // 'test_feature', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // '395', + // 'lasers', + // 'test_feature', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // '4.99', + // 'price', + // 'test_feature', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // 'Hello audience', + // 'message', + // 'test_feature', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Got variable value "%s" for variable "%s" of feature flag "%s"', + // 'OPTIMIZELY', + // '{ "count": 2, "message": "Hello audience" }', + // 'message_info', + // 'test_feature', + // ], + // ]); + }); + + describe('when the variable is not used in the variation', function() { + beforeEach(function() { + sandbox.stub(projectConfig, 'getVariableValueForVariation').returns(null); + }); + + it('returns the variable default value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "new_content" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 14.99); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "price" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 400); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "lasers" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "message" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 1, + message: 'Hello', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "message_info" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "new_content" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 14.99); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "price" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 400); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "lasers" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "message" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the variable default value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 1, + message: 'Hello', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Variable "message_info" is not used in variation "594032". Returning default value.' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + new_content: false, + price: 14.99, + lasers: 400, + message: 'Hello', + message_info: { + count: 1, + message: 'Hello', + }, + }); + // assert.deepEqual(createdLogger.log.args, [ + // [ + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'new_content', + // '594032', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'lasers', + // '594032', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'price', + // '594032', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'message', + // '594032', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Variable "%s" is not used in variation "%s". Returning default value.', + // 'OPTIMIZELY', + // 'message_info', + // '594032', + // ], + // ]); + }); + }); + }); + + describe('when the variation is toggled OFF', function() { + beforeEach(function() { + var experiment = projectConfig.getExperimentFromKey( + optlyInstance.projectConfigManager.getConfig(), + '594037' + ); + var variation = experiment.variations[0]; + var decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.ROLLOUT, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the variable default value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "false".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 14.99); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "14.99".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 400); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "400".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "Hello".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 1, + message: 'Hello', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "{ "count": 1, "message": "Hello" }".' + // ); + }); + + it('returns the variable default value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature', 'new_content', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "false".' + // ); + }); + + it('returns the variable default value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature', 'price', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 14.99); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "14.99".' + // ); + }); + + it('returns the variable default value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature', 'lasers', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 400); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "400".' + // ); + }); + + it('returns the variable default value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString('test_feature', 'message', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Hello'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "Hello".' + // ); + }); + + it('returns the variable default value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature', 'message_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + count: 1, + message: 'Hello', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Feature "test_feature" is not enabled for user user1. Returning the default variable value "{ "count": 1, "message": "Hello" }".' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + new_content: false, + price: 14.99, + lasers: 400, + message: 'Hello', + message_info: { + count: 1, + message: 'Hello', + }, + }); + // assert.deepEqual(createdLogger.log.args, [ + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature', + // 'user1', + // 'false', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature', + // 'user1', + // '400', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature', + // 'user1', + // '14.99', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature', + // 'user1', + // 'Hello', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".', + // 'OPTIMIZELY', + // 'test_feature', + // 'user1', + // '{ "count": 1, "message": "Hello" }', + // ], + // ]); + }); + }); + }); + + describe('not bucketed into an experiment or a rollout ', function() { + beforeEach(function() { + var decisionObj = { + experiment: null, + variation: null, + decisionSource: null, + }; + fakeDecisionResponse = { + result: decisionObj, + reasons: [], + }; + sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); + }); + + it('returns the variable default value from getFeatureVariable when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'is_button_animated', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "is_button_animated" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 50.55); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_width" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 10); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "num_buttons" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_txt" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariable when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_info" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariableBoolean', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1', + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, false); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "is_button_animated" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariableDouble', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 50.55); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_width" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariableInteger', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 10); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "num_buttons" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariableString', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', 'user1', { + test_attribute: 'test_value', + }); + assert.strictEqual(result, 'Buy me'); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_txt" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the variable default value from getFeatureVariableJSON', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + num_buttons: 0, + text: 'default value', + }); + + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_info" of feature flag "test_feature_for_experiment".' + // ); + }); + + it('returns the right values from getAllFeatureVariables', function() { + var result = optlyInstance.getAllFeatureVariables('test_feature_for_experiment', 'user1', { + test_attribute: 'test_value', + }); + assert.deepEqual(result, { + is_button_animated: false, + button_width: 50.55, + num_buttons: 10, + button_txt: 'Buy me', + button_info: { + num_buttons: 0, + text: 'default value', + }, + }); + // assert.deepEqual(createdLogger.log.args, [ + // [ + // LOG_LEVEL.INFO, + // '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".', + // 'OPTIMIZELY', + // 'user1', + // 'num_buttons', + // 'test_feature_for_experiment', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".', + // 'OPTIMIZELY', + // 'user1', + // 'is_button_animated', + // 'test_feature_for_experiment', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".', + // 'OPTIMIZELY', + // 'user1', + // 'button_txt', + // 'test_feature_for_experiment', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".', + // 'OPTIMIZELY', + // 'user1', + // 'button_width', + // 'test_feature_for_experiment', + // ], + // [ + // LOG_LEVEL.INFO, + // '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".', + // 'OPTIMIZELY', + // 'user1', + // 'button_info', + // 'test_feature_for_experiment', + // ], + // ]); + }); + }); + + it('returns null from getFeatureVariable if user id is null when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'is_button_animated', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is undefined when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'is_button_animated', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is not provided when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'is_button_animated'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is null when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is undefined when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is not provided when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_width'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is null when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is undefined when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is not provided when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'num_buttons'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is null when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is undefined when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is not provided when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_txt'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is null when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is undefined when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariable if user id is not provided when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'button_info'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableBoolean when called with a non-boolean variable', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'button_width', 'user1'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Requested variable type "boolean", but variable is of type "double". Use correct API to retrieve value. Returning None.' + // ); + }); + + it('returns null from getFeatureVariableDouble when called with a non-double variable', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1' + ); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Requested variable type "double", but variable is of type "boolean". Use correct API to retrieve value. Returning None.' + // ); + }); + + it('returns null from getFeatureVariableInteger when called with a non-integer variable', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'button_width', 'user1'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Requested variable type "integer", but variable is of type "double". Use correct API to retrieve value. Returning None.' + // ); + }); + + it('returns null from getFeatureVariableString when called with a non-string variable', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'num_buttons', 'user1'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Requested variable type "string", but variable is of type "integer". Use correct API to retrieve value. Returning None.' + // ); + }); + + it('returns null from getFeatureVariableJSON when called with a non-json variable', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_txt', 'user1'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Requested variable type "json", but variable is of type "string". Use correct API to retrieve value. Returning None.' + // ); + }); + + it('returns null from getFeatureVariableBoolean if user id is null', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + null, + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableBoolean if user id is undefined', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + undefined, + { test_attribute: 'test_value' } + ); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableBoolean if user id is not provided', function() { + var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableDouble if user id is null', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableDouble if user id is undefined', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableDouble if user id is not provided', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableInteger if user id is null', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableInteger if user id is undefined', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableInteger if user id is not provided', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableString if user id is null', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableString if user id is undefined', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableString if user id is not provided', function() { + var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableJSON if user id is null', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', null, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableJSON if user id is undefined', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', undefined, { + test_attribute: 'test_value', + }); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + it('returns null from getFeatureVariableJSON if user id is not provided', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info'); + assert.strictEqual(result, null); + // assert.equal( + // buildLogMessageFromArgs(createdLogger.log.lastCall.args), + // 'OPTIMIZELY: Provided user_id is in an invalid format.' + // ); + }); + + describe('type casting failures', function() { + describe('invalid boolean', function() { + beforeEach(function() { + sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('falsezzz'); + }); + + it('should return null and log an error', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'is_button_animated', + 'user1' + ); + assert.strictEqual(result, null); + }); + }); + + describe('invalid integer', function() { + beforeEach(function() { + sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('zzz123'); + }); + + it('should return null and log an error', function() { + var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', 'user1'); + assert.strictEqual(result, null); + }); + }); + + describe('invalid double', function() { + beforeEach(function() { + sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('zzz44.55'); + }); + + it('should return null and log an error', function() { + var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', 'user1'); + assert.strictEqual(result, null); + }); + }); + + describe('invalid json', function() { + beforeEach(function() { + sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('zzz44.55'); + }); + + it('should return null and log an error', function() { + var result = optlyInstance.getFeatureVariableJSON('test_feature_for_experiment', 'button_info', 'user1'); + assert.strictEqual(result, null); + }); + }); + }); + + it('returns null from getFeatureVariable if the argument feature key is invalid when variable type is boolean', function() { + var result = optlyInstance.getFeatureVariable('thisIsNotAValidKey<><><>', 'is_button_animated', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariable if the argument feature key is invalid when variable type is double', function() { + var result = optlyInstance.getFeatureVariable('thisIsNotAValidKey<><><>', 'button_width', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariable if the argument feature key is invalid when variable type is integer', function() { + var result = optlyInstance.getFeatureVariable('thisIsNotAValidKey<><><>', 'num_buttons', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariable if the argument feature key is invalid when variable type is string', function() { + var result = optlyInstance.getFeatureVariable('thisIsNotAValidKey<><><>', 'button_txt', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariable if the argument feature key is invalid when variable type is json', function() { + var result = optlyInstance.getFeatureVariable('thisIsNotAValidKey<><><>', 'button_info', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariable if the argument variable key is invalid', function() { + var result = optlyInstance.getFeatureVariable( + 'test_feature_for_experiment', + 'thisIsNotAVariableKey****', + 'user1' + ); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableBoolean if the argument feature key is invalid', function() { + var result = optlyInstance.getFeatureVariableBoolean('thisIsNotAValidKey<><><>', 'is_button_animated', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableDouble if the argument feature key is invalid', function() { + var result = optlyInstance.getFeatureVariableDouble('thisIsNotAValidKey<><><>', 'button_width', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableInteger if the argument feature key is invalid', function() { + var result = optlyInstance.getFeatureVariableInteger('thisIsNotAValidKey<><><>', 'num_buttons', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableString if the argument feature key is invalid', function() { + var result = optlyInstance.getFeatureVariableString('thisIsNotAValidKey<><><>', 'button_txt', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableJSON if the argument feature key is invalid', function() { + var result = optlyInstance.getFeatureVariableJSON('thisIsNotAValidKey<><><>', 'button_info', 'user1'); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableBoolean if the argument variable key is invalid', function() { + var result = optlyInstance.getFeatureVariableBoolean( + 'test_feature_for_experiment', + 'thisIsNotAVariableKey****', + 'user1' + ); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableDouble if the argument variable key is invalid', function() { + var result = optlyInstance.getFeatureVariableDouble( + 'test_feature_for_experiment', + 'thisIsNotAVariableKey****', + 'user1' + ); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableInteger if the argument variable key is invalid', function() { + var result = optlyInstance.getFeatureVariableInteger( + 'test_feature_for_experiment', + 'thisIsNotAVariableKey****', + 'user1' + ); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableString if the argument variable key is invalid', function() { + var result = optlyInstance.getFeatureVariableString( + 'test_feature_for_experiment', + 'thisIsNotAVariableKey****', + 'user1' + ); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariableJSON if the argument variable key is invalid', function() { + var result = optlyInstance.getFeatureVariableJSON( + 'test_feature_for_experiment', + 'thisIsNotAVariableKey****', + 'user1' + ); + assert.strictEqual(result, null); + }); + + it('returns null from getFeatureVariable when optimizely object is not a valid instance', function() { + const { optlyInstance, errorNotifier, createdLogger } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + sinon.stub(createdLogger, 'error'); + + const val = optlyInstance.getFeatureVariable('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); + assert.strictEqual(val, null); + }); + + it('returns null from getFeatureVariableBoolean when optimizely object is not a valid instance', function() { + var instance = new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + eventDispatcher: eventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + const val = instance.getFeatureVariableBoolean('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); + assert.strictEqual(val, null); + }); + + it('returns null from getFeatureVariableDouble when optimizely object is not a valid instance', function() { + var instance = new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + eventDispatcher: eventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + const val = instance.getFeatureVariableDouble('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); + assert.strictEqual(val, null); + }); + + it('returns null from getFeatureVariableInteger when optimizely object is not a valid instance', function() { + var instance = new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + eventDispatcher: eventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + const val = instance.getFeatureVariableInteger('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); + assert.strictEqual(val, null); + }); + + it('returns null from getFeatureVariableString when optimizely object is not a valid instance', function() { + var instance = new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + eventDispatcher: eventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + const val = instance.getFeatureVariableString('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); + assert.strictEqual(val, null); + }); + + it('returns null from getFeatureVariableJSON when optimizely object is not a valid instance', function() { + var instance = new Optimizely({ + projectConfigManager: getMockProjectConfigManager(), + eventDispatcher: eventDispatcher, + logger: createdLogger, + notificationCenter, + eventProcessor, + }); + + const val = instance.getFeatureVariableJSON('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); + assert.strictEqual(val, null); + }); + }); + }); + + describe('audience match types', function() { + var sandbox = sinon.sandbox.create(); + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + var optlyInstance; + var notificationCenter = createNotificationCenter({ logger: createdLogger, }); + var eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTypedAudiencesConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + eventProcessor, + notificationCenter, + }); + + sandbox.stub(eventDispatcher, 'dispatchEvent'); + sandbox.stub(createdLogger, 'log'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('can activate an experiment with a typed audience', function() { + var variationKey = optlyInstance.activate('typed_audience_experiment', 'user1', { + // Should be included via exact match string audience with id '3468206642' + house: 'Gryffindor', + }); + assert.strictEqual(variationKey, 'A'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + assert.includeDeepMembers(eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, [ + { entity_id: '594015', key: 'house', type: 'custom', value: 'Gryffindor' }, + ]); + + variationKey = optlyInstance.activate('typed_audience_experiment', 'user1', { + // Should be included via exact match number audience with id '3468206646' + lasers: 45.5, + }); + assert.strictEqual(variationKey, 'A'); + sinon.assert.calledTwice(eventDispatcher.dispatchEvent); + assert.includeDeepMembers(eventDispatcher.dispatchEvent.getCall(1).args[0].params.visitors[0].attributes, [ + { entity_id: '594016', key: 'lasers', type: 'custom', value: 45.5 }, + ]); + }); + + it('can exclude a user from an experiment with a typed audience via activate', function() { + var variationKey = optlyInstance.activate('typed_audience_experiment', 'user1', { + house: 'Hufflepuff', + }); + assert.isNull(variationKey); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('can track an experiment with a typed audience', function() { + optlyInstance.track('item_bought', 'user1', { + // Should be included via substring match string audience with id '3988293898' + house: 'Welcome to Slytherin!', + }); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + assert.includeDeepMembers(eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, [ + { entity_id: '594015', key: 'house', type: 'custom', value: 'Welcome to Slytherin!' }, + ]); + }); + + it('can include a user in a rollout with a typed audience via isFeatureEnabled', function() { + var featureEnabled = optlyInstance.isFeatureEnabled('feat', 'user1', { + // Should be included via exists match audience with id '3988293899' + favorite_ice_cream: 'chocolate', + }); + assert.isTrue(featureEnabled); + + featureEnabled = optlyInstance.isFeatureEnabled('feat', 'user1', { + // Should be included via less-than match audience with id '3468206644' + lasers: -3, + }); + assert.isTrue(featureEnabled); + }); + + it('can exclude a user from a rollout with a typed audience via isFeatureEnabled', function() { + var featureEnabled = optlyInstance.isFeatureEnabled('feat', 'user1', {}); + assert.isFalse(featureEnabled); + }); + + it('can return a variable value from a feature test with a typed audience via getFeatureVariable', function() { + var variableValue = optlyInstance.getFeatureVariable('feat_with_var', 'x', 'user1', { + // Should be included in the feature test via greater-than match audience with id '3468206647' + lasers: 71, + }); + assert.strictEqual(variableValue, 'xyz'); + + variableValue = optlyInstance.getFeatureVariable('feat_with_var', 'x', 'user1', { + // Should be included in the feature test via exact match boolean audience with id '3468206643' + should_do_it: true, + }); + assert.strictEqual(variableValue, 'xyz'); + }); + + it('can return a variable value from a feature test with a typed audience via getFeatureVariableString', function() { + var variableValue = optlyInstance.getFeatureVariableString('feat_with_var', 'x', 'user1', { + // Should be included in the feature test via greater-than match audience with id '3468206647' + lasers: 71, + }); + assert.strictEqual(variableValue, 'xyz'); + + variableValue = optlyInstance.getFeatureVariableString('feat_with_var', 'x', 'user1', { + // Should be included in the feature test via exact match boolean audience with id '3468206643' + should_do_it: true, + }); + assert.strictEqual(variableValue, 'xyz'); + }); + + it('can return the default value from a feature variable from getFeatureVariable, via excluding a user from a feature test with a typed audience', function() { + var variableValue = optlyInstance.getFeatureVariable('feat_with_var', 'x', 'user1', { + lasers: 50, + }); + assert.strictEqual(variableValue, 'x'); + }); + + it('can return the default value from a feature variable from getFeatureVariableString, via excluding a user from a feature test with a typed audience', function() { + var variableValue = optlyInstance.getFeatureVariableString('feat_with_var', 'x', 'user1', { + lasers: 50, + }); + assert.strictEqual(variableValue, 'x'); + }); + }); + + describe('audience combinations', function() { + var sandbox = sinon.sandbox.create(); + var evalSpy; + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + var optlyInstance; + var audienceEvaluator; + var notificationCenter = createNotificationCenter({ logger: createdLogger, }); + var eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + beforeEach(function() { + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTypedAudiencesConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + audienceEvaluator = AudienceEvaluator.prototype; + + sandbox.stub(eventDispatcher, 'dispatchEvent'); + + sandbox.stub(createdLogger, 'log'); + evalSpy = sandbox.spy(audienceEvaluator, 'evaluate'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('can activate an experiment with complex audience conditions', function() { + var variationKey = optlyInstance.activate('audience_combinations_experiment', 'user1', { + // Should be included via substring match string audience with id '3988293898', and + // exact match number audience with id '3468206646' + house: 'Welcome to Slytherin!', + lasers: 45.5, + }); + assert.strictEqual(variationKey, 'A'); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + assert.includeDeepMembers(eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, [ + { entity_id: '594015', key: 'house', type: 'custom', value: 'Welcome to Slytherin!' }, + { entity_id: '594016', key: 'lasers', type: 'custom', value: 45.5 }, + ]); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().experiments[2].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Welcome to Slytherin!', lasers: 45.5 }); + }); + + it('can exclude a user from an experiment with complex audience conditions', function() { + var variationKey = optlyInstance.activate('audience_combinations_experiment', 'user1', { + // Should be excluded - substring string audience with id '3988293898' does not match, + // so the overall conditions fail + house: 'Hufflepuff', + lasers: 45.5, + }); + assert.isNull(variationKey); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().experiments[2].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Hufflepuff', lasers: 45.5 }); + }); + + it('can track an experiment with complex audience conditions', function() { + optlyInstance.track('user_signed_up', 'user1', { + // Should be included via exact match string audience with id '3468206642', and + // exact match boolean audience with id '3468206643' + house: 'Gryffindor', + should_do_it: true, + }); + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + assert.includeDeepMembers(eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, [ + { entity_id: '594015', key: 'house', type: 'custom', value: 'Gryffindor' }, + { entity_id: '594017', key: 'should_do_it', type: 'custom', value: true }, + ]); + }); + + it('can include a user in a rollout with complex audience conditions via isFeatureEnabled', function() { + var featureEnabled = optlyInstance.isFeatureEnabled('feat2', 'user1', { + // Should be included via substring match string audience with id '3988293898', and + // exists audience with id '3988293899' + house: '...Slytherinnn...sss.', + favorite_ice_cream: 'matcha', + }); + assert.isTrue(featureEnabled); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().rollouts[2].experiments[0].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { + house: '...Slytherinnn...sss.', + favorite_ice_cream: 'matcha', + }); + }); + + it('can exclude a user from a rollout with complex audience conditions via isFeatureEnabled', function() { + var featureEnabled = optlyInstance.isFeatureEnabled('feat2', 'user1', { + // Should be excluded - substring match string audience with id '3988293898' does not match, + // and no audience in the other branch of the 'and' matches either + house: 'Lannister', + }); + assert.isFalse(featureEnabled); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().rollouts[2].experiments[0].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Lannister' }); + }); + + it('can return a variable value from a feature test with complex audience conditions via getFeatureVariableString', function() { + var variableValue = optlyInstance.getFeatureVariableInteger('feat2_with_var', 'z', 'user1', { + // Should be included via exact match string audience with id '3468206642', and + // greater than audience with id '3468206647' + house: 'Gryffindor', + lasers: 700, + }); + assert.strictEqual(variableValue, 150); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Gryffindor', lasers: 700 }); + }); + + it('can return a variable value from a feature test with complex audience conditions via getFeatureVariable', function() { + var variableValue = optlyInstance.getFeatureVariable('feat2_with_var', 'z', 'user1', { + // Should be included via exact match string audience with id '3468206642', and + // greater than audience with id '3468206647' + house: 'Gryffindor', + lasers: 700, + }); + assert.strictEqual(variableValue, 150); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Gryffindor', lasers: 700 }); + }); + + it('can return the default value for a feature variable from getFeatureVariable, via excluding a user from a feature test with complex audience conditions', function() { + var variableValue = optlyInstance.getFeatureVariable('feat2_with_var', 'z', 'user1', { + // Should be excluded - no audiences match with no attributes + }); + assert.strictEqual(variableValue, 10); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), {}); + }); + + it('can return the default value for a feature variable from getFeatureVariableString, via excluding a user from a feature test with complex audience conditions', function() { + var variableValue = optlyInstance.getFeatureVariableInteger('feat2_with_var', 'z', 'user1', { + // Should be excluded - no audiences match with no attributes + }); + assert.strictEqual(variableValue, 10); + sinon.assert.calledWithExactly( + audienceEvaluator.evaluate, + optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, + optlyInstance.projectConfigManager.getConfig().audiencesById, + sinon.match.any + ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), {}); + }); + }); + + describe('event batching', function() { + var bucketStub; + var fakeDecisionResponse; + var notificationCenter; + var eventDispatcher; + var eventProcessor; + + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + + beforeEach(function() { + bucketStub = sinon.stub(bucketer, 'bucket'); + + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); + notificationCenter = createNotificationCenter({ logger: createdLogger, }); + eventDispatcher = getMockEventDispatcher(); + eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + bucketer.bucket.restore(); + + createdLogger.debug.restore(); + createdLogger.info.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + fns.uuid.restore(); + }); + + describe('close method', function() { + var eventProcessorStopPromise; + var optlyInstance; + var mockEventProcessor; + beforeEach(function() { + mockEventProcessor = { + process: sinon.stub(), + start: sinon.stub(), + stop: sinon.stub(), + onRunning: sinon.stub(), + onTerminated: sinon.stub(), + onDispatch: sinon.stub(), + setLogger: sinon.stub(), + }; + }); + + describe('when the event processor onTerminated method returns a promise that fulfills', function() { + beforeEach(function() { + eventProcessorStopPromise = Promise.resolve(); + mockEventProcessor.onTerminated.returns(eventProcessorStopPromise); + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 3, + eventFlushInterval: 100, + eventProcessor: mockEventProcessor, + notificationCenter, + }); + }); + + afterEach(function() { + return eventProcessorStopPromise.catch(function() { + // Handle rejected promise - don't want test to fail + }); + }); + + it('returns a promise that resolves', function() { + return optlyInstance.close().then().catch(() => { + assert.fail(); + }) + }); + }); + + describe('when the event processor onTerminated() method returns a promise that rejects', function() { + beforeEach(function() { + eventProcessorStopPromise = Promise.reject(new Error('FAILED_TO_STOP')); + eventProcessorStopPromise.catch(() => {}); + mockEventProcessor.onTerminated.returns(eventProcessorStopPromise); + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 3, + eventFlushInterval: 100, + eventProcessor: mockEventProcessor, + notificationCenter, + }); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + return eventProcessorStopPromise.catch(function() { + // Handle rejected promise - don't want test to fail + }); + }); + + it('returns a promise that rejects', function() { + return optlyInstance.close().then(() => { + assert.fail('promnise should reject') + }).catch(() => { + + }); + }); + }); + }); + }); + + describe('project config management', function() { + var createdLogger = createLogger({ + logLevel: LOG_LEVEL.INFO, + logToConsole: false, + }); + + var notificationCenter = createNotificationCenter({ logger: createdLogger, }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher + ); + + beforeEach(function() { + + sinon.stub(createdLogger, 'debug'); + sinon.stub(createdLogger, 'info'); + sinon.stub(createdLogger, 'warn'); + sinon.stub(createdLogger, 'error'); + }); + + afterEach(function() { + createdLogger.debug.restore(); + createdLogger.info.restore(); + createdLogger.warn.restore(); + createdLogger.error.restore(); + eventDispatcher.dispatchEvent.reset(); + + }); + + var optlyInstance; + + it('should call the project config manager stop method when the close method is called', function() { + const projectConfigManager = getMockProjectConfigManager(); + sinon.stub(projectConfigManager, 'stop'); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + + projectConfigManager, + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + eventProcessor, + notificationCenter, + }); + optlyInstance.close(); + sinon.assert.calledOnce(projectConfigManager.stop); + }); + + describe('when no project config is available yet ', function() { + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager(), + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + }); + + + it('returns fallback values from API methods that return meaningful values', function() { + assert.isNull(optlyInstance.activate('my_experiment', 'user1')); + assert.isNull(optlyInstance.getVariation('my_experiment', 'user1')); + assert.isFalse(optlyInstance.setForcedVariation('my_experiment', 'user1', 'variation_1')); + assert.isNull(optlyInstance.getForcedVariation('my_experiment', 'user1')); + assert.isFalse(optlyInstance.isFeatureEnabled('my_feature', 'user1')); + assert.deepEqual(optlyInstance.getEnabledFeatures('user1'), []); + assert.isNull(optlyInstance.getFeatureVariable('my_feature', 'my_var', 'user1')); + assert.isNull(optlyInstance.getFeatureVariableBoolean('my_feature', 'my_bool_var', 'user1')); + assert.isNull(optlyInstance.getFeatureVariableDouble('my_feature', 'my_double_var', 'user1')); + assert.isNull(optlyInstance.getFeatureVariableInteger('my_feature', 'my_int_var', 'user1')); + assert.isNull(optlyInstance.getFeatureVariableString('my_feature', 'my_str_var', 'user1')); + }); + + it('does not dispatch any events in API methods that dispatch events', function() { + optlyInstance.activate('my_experiment', 'user1'); + optlyInstance.track('my_event', 'user1'); + optlyInstance.isFeatureEnabled('my_feature', 'user1'); + optlyInstance.getEnabledFeatures('user1'); + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + }); + + describe('onReady method', function() { + var setTimeoutSpy; + var clearTimeoutSpy; + beforeEach(function() { + setTimeoutSpy = sinon.spy(clock, 'setTimeout'); + clearTimeoutSpy = sinon.spy(clock, 'clearTimeout'); + }); + + afterEach(function() { + setTimeoutSpy.restore(); + clearTimeoutSpy.restore(); + }); + + it('fulfills the promise after the project config manager onRunning promise is fulfilled', function() { + const projectConfigManager = getMockProjectConfigManager(); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + + return optlyInstance.onReady(); + }); + + it('rejects the promise after the timeout has expired when the project config manager onReady promise still has not resolved', function() { + const projectConfigManager = getMockProjectConfigManager({ onRunning: new Promise(function() {}) }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', projectConfigManager, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + var readyPromise = optlyInstance.onReady({ timeout: 500 }); + clock.tick(501); + return readyPromise.then(() => { + return Promise.reject(new Error('PROMISE_SHOULD_NOT_HAVE_RESOLVED')); + }, (err) => { + assert.equal(err.message, sprintf(ONREADY_TIMEOUT, 500)); + }); + }); + + it('rejects the promise after 30 seconds when no timeout argument is provided and the project config manager onReady promise still has not resolved', function() { + const projectConfigManager = getMockProjectConfigManager({ onRunning: new Promise(function() {}) }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + var readyPromise = optlyInstance.onReady(); + clock.tick(300001); + return readyPromise.then(() => { + return Promise.reject(new Error('PROMISE_SHOULD_NOT_HAVE_RESOLVED')); + }, (err) => { + assert.equal(err.message, sprintf(ONREADY_TIMEOUT, 30000)); + }); + }); + + it('rejects the promise after the instance is closed', function() { + const projectConfigManager = getMockProjectConfigManager({ onRunning: new Promise(function() {}) }); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + var readyPromise = optlyInstance.onReady({ timeout: 100 }); + + optlyInstance.close(); + + return readyPromise.then(() => { + return Promise.reject(new Error('PROMISE_SHOULD_NOT_HAVE_RESOLVED')); + }, (err) => { + assert.equal(err.baseMessage, SERVICE_STOPPED_BEFORE_RUNNING); + }); + }); + + it('can be called several times with different timeout values and the returned promises behave correctly', function() { + const onRunning = resolvablePromise(); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager({ + onRunning: onRunning.promise, + }), + + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + var readyPromise1 = optlyInstance.onReady({ timeout: 100 }); + var readyPromise2 = optlyInstance.onReady({ timeout: 200 }); + var readyPromise3 = optlyInstance.onReady({ timeout: 300 }); + clock.tick(101); + return readyPromise1 + .catch(function() { + clock.tick(100); + return readyPromise2; + }) + .catch(function() { + // readyPromise3 has not resolved yet because only 201 ms have elapsed. + // Calling close on the instance should resolve readyPromise3 + optlyInstance.close().catch(() => {}); + return readyPromise3; + }).catch(() => {}); + }); + + it('clears the timeout when the project config manager ready promise fulfills', function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager(), + eventProcessor, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + notificationCenter, + eventProcessor, + }); + return optlyInstance.onReady().then(function() { + sinon.assert.calledOnce(clock.setTimeout); + var timeout = clock.setTimeout.getCall(0).returnValue; + sinon.assert.calledOnce(clock.clearTimeout); + sinon.assert.calledWithExactly(clock.clearTimeout, timeout); + }); + }); + }); + + describe('project config updates', function() { + var fakeProjectConfigManager; + beforeEach(function() { + fakeProjectConfigManager = getMockProjectConfigManager(), + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + eventProcessor, + projectConfigManager: fakeProjectConfigManager, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + sdkKey: '12345', + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + }); + + it('uses the newest project config object from project config manager', function() { + // Should start off returning false/null - no project config available + assert.isFalse(optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user45678')); + assert.isNull(optlyInstance.activate('myOtherExperiment', 'user98765')); + + // Project config manager receives new project config object - should use this now + + const datafile = testData.getTestProjectConfigWithFeatures(); + + const newConfig = createProjectConfig(datafile, JSON.stringify(datafile)); + + fakeProjectConfigManager.setConfig(newConfig); + fakeProjectConfigManager.pushUpdate(newConfig); + + // With the new project config containing this feature, should return true + assert.isTrue(optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user45678')); + + // Update to another project config containing a new experiment + var differentDatafile = testData.getTestProjectConfigWithFeatures(); + differentDatafile.experiments.push({ + key: 'myOtherExperiment', + status: 'Running', + forcedVariations: {}, + audienceIds: [], + layerId: '5', + trafficAllocation: [ + { + entityId: '99999999', + endOfRange: 10000, + }, + ], + id: '999998888777776', + variations: [ + { + key: 'control', + id: '99999999', + }, + ], + }); + differentDatafile.revision = '44'; + var differentConfig = createProjectConfig(differentDatafile, JSON.stringify(differentDatafile)); + fakeProjectConfigManager.setConfig(differentConfig); + fakeProjectConfigManager.pushUpdate(differentConfig); + + // activate should return a variation for the new experiment + assert.strictEqual(optlyInstance.activate('myOtherExperiment', 'user98765'), 'control'); + }); + + it('emits a notification when the project config manager emits a new project config object', function() { + var listener = sinon.spy(); + optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, + listener + ); + var newConfig = createProjectConfig(testData.getTestProjectConfigWithFeatures()); + fakeProjectConfigManager.pushUpdate(newConfig); + + sinon.assert.calledOnce(listener); + }); + }); + }); + + describe('log event notification', function() { + var optlyInstance; + var bucketStub; + var fakeDecisionResponse; + var eventDispatcherSpy; + var logger = createLogger(); + var notificationCenter = createNotificationCenter({ logger }); + var eventProcessor; + beforeEach(function() { + bucketStub = sinon.stub(bucketer, 'bucket'); + eventDispatcherSpy = sinon.spy(() => Promise.resolve({ statusCode: 200 })); + eventProcessor = getForwardingEventProcessor( + { dispatchEvent: eventDispatcherSpy }, + ); + + const datafile = testData.getTestProjectConfig(); + const mockConfigManager = getMockProjectConfigManager(); + mockConfigManager.setConfig(createProjectConfig(datafile, JSON.stringify(datafile))); + + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + logger, + isValidInstance: true, + eventBatchSize: 1, + notificationCenter, + eventProcessor, + }); + }); + + afterEach(function() { + bucketer.bucket.restore(); + optlyInstance.close(); + }); + + it('should trigger a log event notification when an impression event is dispatched', function() { + var notificationListener = sinon.spy(); + optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + notificationListener + ); + fakeDecisionResponse = { + result: '111129', + reasons: [], + }; + bucketStub.returns(fakeDecisionResponse); + var activate = optlyInstance.activate('testExperiment', 'testUser'); + assert.strictEqual(activate, 'variation'); + sinon.assert.calledOnce(eventDispatcherSpy); + sinon.assert.calledOnce(notificationListener); + sinon.assert.calledWithExactly(notificationListener, eventDispatcherSpy.getCall(0).args[0]); + }); + + it('should trigger a log event notification when a conversion event is dispatched', function() { + var notificationListener = sinon.spy(); + optlyInstance.notificationCenter.addNotificationListener( + NOTIFICATION_TYPES.LOG_EVENT, + notificationListener + ); + optlyInstance.track('testEvent', 'testUser'); + sinon.assert.calledOnce(eventDispatcherSpy); + sinon.assert.calledOnce(notificationListener); + sinon.assert.calledWithExactly(notificationListener, eventDispatcherSpy.getCall(0).args[0]); + }); + }); +}); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts new file mode 100644 index 000000000..b8707a006 --- /dev/null +++ b/lib/optimizely/index.ts @@ -0,0 +1,1801 @@ +/** + * Copyright 2020-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from '../logging/logger'; +import { sprintf, objectValues } from '../utils/fns'; +import { createNotificationCenter, DefaultNotificationCenter } from '../notification_center'; +import { EventProcessor } from '../event_processor/event_processor'; + +import { OdpManager } from '../odp/odp_manager'; +import { VuidManager } from '../vuid/vuid_manager'; +import { OdpEvent } from '../odp/event_manager/odp_event'; +import { OptimizelySegmentOption } from '../odp/segment_manager/optimizely_segment_option'; +import { BaseService, ServiceState } from '../service'; + +import { + UserAttributes, + EventTags, + OptimizelyConfig, + UserProfileService, + Variation, + FeatureFlag, + FeatureVariable, + OptimizelyDecideOption, + FeatureVariableValue, + OptimizelyDecision, + Client, + UserProfileServiceAsync, + isHoldout, +} from '../shared_types'; +import { newErrorDecision } from '../optimizely_decision'; +import OptimizelyUserContext from '../optimizely_user_context'; +import { ProjectConfigManager } from '../project_config/project_config_manager'; +import { createDecisionService, DecisionService, DecisionObj } from '../core/decision_service'; +import { buildLogEvent } from '../event_processor/event_builder/log_event'; +import { buildImpressionEvent, buildConversionEvent } from '../event_processor/event_builder/user_event'; +import { isSafeInteger } from '../utils/fns'; +import { validate } from '../utils/attributes_validator'; +import * as eventTagsValidator from '../utils/event_tags_validator'; +import * as projectConfig from '../project_config/project_config'; +import * as userProfileServiceValidator from '../utils/user_profile_service_validator'; +import * as stringValidator from '../utils/string_value_validator'; +import * as decision from '../core/decision'; + +import { + DECISION_SOURCES, + DECISION_MESSAGES, + FEATURE_VARIABLE_TYPES, + NODE_CLIENT_ENGINE, + CLIENT_VERSION, +} from '../utils/enums'; +import { Fn, Maybe, OpType } from '../utils/type'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; + +import { NOTIFICATION_TYPES, DecisionNotificationType, DECISION_NOTIFICATION_TYPES, ActivateListenerPayload } from '../notification_center/type'; +import { + FEATURE_NOT_IN_DATAFILE, + INVALID_INPUT_FORMAT, + NO_EVENT_PROCESSOR, + ODP_EVENT_FAILED, + ODP_EVENT_FAILED_ODP_MANAGER_MISSING, + UNABLE_TO_GET_VUID_VUID_MANAGER_NOT_AVAILABLE, + UNRECOGNIZED_DECIDE_OPTION, + NO_PROJECT_CONFIG_FAILURE, + EVENT_KEY_NOT_FOUND, + NOT_TRACKING_USER, + VARIABLE_REQUESTED_WITH_WRONG_TYPE, +} from 'error_message'; + +import { + FEATURE_ENABLED_FOR_USER, + FEATURE_NOT_ENABLED_FOR_USER, + FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE, + INVALID_CLIENT_ENGINE, + INVALID_DECIDE_OPTIONS, + INVALID_DEFAULT_DECIDE_OPTIONS, + INVALID_EXPERIMENT_KEY_INFO, + NOT_ACTIVATING_USER, + SHOULD_NOT_DISPATCH_ACTIVATE, + TRACK_EVENT, + UPDATED_OPTIMIZELY_CONFIG, + USER_RECEIVED_DEFAULT_VARIABLE_VALUE, + USER_RECEIVED_VARIABLE_VALUE, + VALID_USER_PROFILE_SERVICE, + VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE, +} from 'log_message'; + +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; + +import { ErrorNotifier } from '../error/error_notifier'; +import { ErrorReporter } from '../error/error_reporter'; +import { OptimizelyError } from '../error/optimizly_error'; +import { Value } from '../utils/promise/operation_value'; +import { CmabService } from '../core/decision_service/cmab/cmab_service'; + +const DEFAULT_ONREADY_TIMEOUT = 30000; + +// TODO: Make feature_key, user_id, variable_key, experiment_key, event_key camelCase +type InputKey = 'feature_key' | 'user_id' | 'variable_key' | 'experiment_key' | 'event_key' | 'variation_id'; + +type StringInputs = Partial<Record<InputKey, unknown>>; + +type DecisionReasons = (string | number)[]; + +export const INSTANCE_CLOSED = 'Instance closed'; +export const ONREADY_TIMEOUT = 'onReady timeout expired after %s ms'; +export const INVALID_IDENTIFIER = 'Invalid identifier'; +export const INVALID_ATTRIBUTES = 'Invalid attributes'; + +/** + * options required to create optimizely object + */ +export type OptimizelyOptions = { + projectConfigManager: ProjectConfigManager; + UNSTABLE_conditionEvaluators?: unknown; + cmabService: CmabService; + clientEngine: string; + clientVersion?: string; + errorNotifier?: ErrorNotifier; + eventProcessor?: EventProcessor; + jsonSchemaValidator?: { + validate(jsonObject: unknown): boolean; + }; + logger?: LoggerFacade; + userProfileService?: UserProfileService | null; + userProfileServiceAsync?: UserProfileServiceAsync | null; + defaultDecideOptions?: OptimizelyDecideOption[]; + odpManager?: OdpManager; + vuidManager?: VuidManager + disposable?: boolean; +} + +export default class Optimizely extends BaseService implements Client { + private cleanupTasks: Map<number, Fn> = new Map(); + private nextCleanupTaskId = 0; + private clientEngine: string; + private clientVersion: string; + private errorNotifier?: ErrorNotifier; + private errorReporter: ErrorReporter; + private projectConfigManager: ProjectConfigManager; + private decisionService: DecisionService; + private eventProcessor?: EventProcessor; + private defaultDecideOptions: { [key: string]: boolean }; + private odpManager?: OdpManager; + public notificationCenter: DefaultNotificationCenter; + private vuidManager?: VuidManager; + + constructor(config: OptimizelyOptions) { + super(); + + let clientEngine = config.clientEngine; + if (!clientEngine) { + config.logger?.info(INVALID_CLIENT_ENGINE, clientEngine); + clientEngine = NODE_CLIENT_ENGINE; + } + + this.clientEngine = clientEngine; + this.clientVersion = config.clientVersion || CLIENT_VERSION; + this.errorNotifier = config.errorNotifier; + this.logger = config.logger; + this.projectConfigManager = config.projectConfigManager; + this.errorReporter = new ErrorReporter(this.logger, this.errorNotifier); + this.odpManager = config.odpManager; + this.vuidManager = config.vuidManager; + this.eventProcessor = config.eventProcessor; + + if(config.disposable) { + this.projectConfigManager.makeDisposable(); + this.eventProcessor?.makeDisposable(); + this.odpManager?.makeDisposable(); + } + + // pass a child logger to sub-components + if (this.logger) { + this.projectConfigManager.setLogger(this.logger.child()); + this.eventProcessor?.setLogger(this.logger.child()); + this.odpManager?.setLogger(this.logger.child()); + } + + let decideOptionsArray = config.defaultDecideOptions ?? []; + + if (!Array.isArray(decideOptionsArray)) { + this.logger?.debug(INVALID_DEFAULT_DECIDE_OPTIONS); + decideOptionsArray = []; + } + + const defaultDecideOptions: { [key: string]: boolean } = {}; + decideOptionsArray.forEach(option => { + // Filter out all provided default decide options that are not in OptimizelyDecideOption[] + if (OptimizelyDecideOption[option]) { + defaultDecideOptions[option] = true; + } else { + this.logger?.warn(UNRECOGNIZED_DECIDE_OPTION, option); + } + }); + this.defaultDecideOptions = defaultDecideOptions; + + this.projectConfigManager = config.projectConfigManager; + this.projectConfigManager.onUpdate((configObj: projectConfig.ProjectConfig) => { + this.logger?.info( + UPDATED_OPTIMIZELY_CONFIG, + configObj.revision, + configObj.projectId + ); + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, undefined); + + this.updateOdpSettings(); + }); + + this.eventProcessor = config.eventProcessor; + this.eventProcessor?.onDispatch((event) => { + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.LOG_EVENT, event); + }); + + this.odpManager = config.odpManager; + + let userProfileService: Maybe<UserProfileService> = undefined; + if (config.userProfileService) { + try { + if (userProfileServiceValidator.validate(config.userProfileService)) { + userProfileService = config.userProfileService; + this.logger?.info(VALID_USER_PROFILE_SERVICE); + } + } catch (ex) { + this.logger?.warn(ex); + } + } + + this.decisionService = createDecisionService({ + userProfileService: userProfileService, + userProfileServiceAsync: config.userProfileServiceAsync || undefined, + cmabService: config.cmabService, + logger: this.logger, + UNSTABLE_conditionEvaluators: config.UNSTABLE_conditionEvaluators, + }); + + this.notificationCenter = createNotificationCenter({ logger: this.logger, errorNotifier: this.errorNotifier }); + + this.start(); + } + + start(): void { + if (!this.isNew()) { + return; + } + + super.start(); + + this.state = ServiceState.Starting; + this.projectConfigManager.start(); + this.eventProcessor?.start(); + this.odpManager?.start(); + + Promise.all([ + this.projectConfigManager.onRunning(), + this.eventProcessor ? this.eventProcessor.onRunning() : Promise.resolve(), + this.odpManager ? this.odpManager.onRunning() : Promise.resolve(), + this.vuidManager ? this.vuidManager.initialize() : Promise.resolve(), + ]).then(() => { + this.state = ServiceState.Running; + this.startPromise.resolve(); + + const vuid = this.vuidManager?.getVuid(); + if (vuid) { + this.odpManager?.setVuid(vuid); + } + }).catch((err) => { + this.state = ServiceState.Failed; + this.errorReporter.report(err); + this.startPromise.reject(err); + }); + } + + /** + * Returns the project configuration retrieved from projectConfigManager + * @return {projectConfig.ProjectConfig} + */ + getProjectConfig(): projectConfig.ProjectConfig | null { + return this.projectConfigManager.getConfig() || null; + } + + /** + * Buckets visitor and sends impression event to Optimizely. + * @param {string} experimentKey + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string|null} variation key + */ + activate(experimentKey: string, userId: string, attributes?: UserAttributes): string | null { + try { + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'activate'); + return null; + } + + if (!this.validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { + return this.notActivatingExperiment(experimentKey, userId); + } + + try { + const variationKey = this.getVariation(experimentKey, userId, attributes); + if (variationKey === null) { + return this.notActivatingExperiment(experimentKey, userId); + } + + // If experiment is not set to 'Running' status, log accordingly and return variation key + if (!projectConfig.isRunning(configObj, experimentKey)) { + this.logger?.debug(SHOULD_NOT_DISPATCH_ACTIVATE, experimentKey); + return variationKey; + } + + const experiment = projectConfig.getExperimentFromKey(configObj, experimentKey); + const variation = experiment.variationKeyMap[variationKey]; + const decisionObj = { + experiment: experiment, + variation: variation, + decisionSource: DECISION_SOURCES.EXPERIMENT, + }; + + this.sendImpressionEvent(decisionObj, '', userId, true, attributes); + return variationKey; + } catch (ex) { + this.logger?.info(NOT_ACTIVATING_USER, userId, experimentKey); + this.errorReporter.report(ex); + return null; + } + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Create an impression event and call the event dispatcher's dispatch method to + * send this event to Optimizely. Then use the notification center to trigger + * any notification listeners for the ACTIVATE notification type. + * @param {DecisionObj} decisionObj Decision Object + * @param {string} flagKey Key for a feature flag + * @param {string} userId ID of user to whom the variation was shown + * @param {UserAttributes} attributes Optional user attributes + * @param {boolean} enabled Boolean representing if feature is enabled + */ + private sendImpressionEvent( + decisionObj: DecisionObj, + flagKey: string, + userId: string, + enabled: boolean, + attributes?: UserAttributes + ): void { + if (!this.eventProcessor) { + this.logger?.error(NO_EVENT_PROCESSOR); + return; + } + + const configObj = this.getProjectConfig(); + if (!configObj) { + return; + } + const impressionEvent = buildImpressionEvent({ + decisionObj: decisionObj, + flagKey: flagKey, + enabled: enabled, + userId: userId, + userAttributes: attributes, + clientEngine: this.clientEngine, + clientVersion: this.clientVersion, + configObj: configObj, + }); + + this.eventProcessor.process(impressionEvent); + + const logEvent = buildLogEvent([impressionEvent]); + + const activateNotificationPayload: ActivateListenerPayload = { + experiment: null, + holdout: null, + userId: userId, + attributes: attributes, + variation: decisionObj.variation, + logEvent, + }; + + if (decisionObj.experiment) { + if (isHoldout(decisionObj.experiment)) { + activateNotificationPayload.holdout = decisionObj.experiment; + } else { + activateNotificationPayload.experiment = decisionObj.experiment; + } + } + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, activateNotificationPayload); + } + + /** + * Sends conversion event to Optimizely. + * @param {string} eventKey + * @param {string} userId + * @param {UserAttributes} attributes + * @param {EventTags} eventTags Values associated with the event. + */ + track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void { + try { + if (!this.eventProcessor) { + this.logger?.error(NO_EVENT_PROCESSOR); + return; + } + + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'track'); + return; + } + + if (!this.validateInputs({ user_id: userId, event_key: eventKey }, attributes, eventTags)) { + return; + } + + + if (!projectConfig.eventWithKeyExists(configObj, eventKey)) { + this.logger?.warn(EVENT_KEY_NOT_FOUND, eventKey); + this.logger?.warn(NOT_TRACKING_USER, userId); + return; + } + + // remove null values from eventTags + eventTags = this.filterEmptyValues(eventTags); + const conversionEvent = buildConversionEvent({ + eventKey: eventKey, + eventTags: eventTags, + userId: userId, + userAttributes: attributes, + clientEngine: this.clientEngine, + clientVersion: this.clientVersion, + configObj: configObj, + }, this.logger); + this.logger?.info(TRACK_EVENT, eventKey, userId); + // TODO is it okay to not pass a projectConfig as second argument + this.eventProcessor.process(conversionEvent); + + const logEvent = buildLogEvent([conversionEvent]); + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.TRACK, { + eventKey, + userId, + attributes, + eventTags, + logEvent, + }); + } catch (e) { + this.errorReporter.report(e); + this.logger?.error(NOT_TRACKING_USER, userId); + } + } + + /** + * Gets variation where visitor will be bucketed. + * @param {string} experimentKey + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string|null} variation key + */ + getVariation(experimentKey: string, userId: string, attributes?: UserAttributes): string | null { + try { + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getVariation'); + return null; + } + + try { + if (!this.validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { + return null; + } + + const experiment = configObj.experimentKeyMap[experimentKey]; + if (!experiment || experiment.isRollout) { + this.logger?.debug(INVALID_EXPERIMENT_KEY_INFO, experimentKey); + return null; + } + + const variationKey = this.decisionService.getVariation( + configObj, + experiment, + this.createInternalUserContext(userId, attributes) as OptimizelyUserContext + ).result; + const decisionNotificationType: DecisionNotificationType = projectConfig.isFeatureExperiment(configObj, experiment.id) + ? DECISION_NOTIFICATION_TYPES.FEATURE_TEST + : DECISION_NOTIFICATION_TYPES.AB_TEST; + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: decisionNotificationType, + userId: userId, + attributes: attributes || {}, + decisionInfo: { + experimentKey: experimentKey, + variationKey: variationKey, + }, + }); + + return variationKey; + } catch (ex) { + this.errorReporter.report(ex); + return null; + } + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Force a user into a variation for a given experiment. + * @param {string} experimentKey + * @param {string} userId + * @param {string|null} variationKey user will be forced into. If null, + * then clear the existing experiment-to-variation mapping. + * @return {boolean} A boolean value that indicates if the set completed successfully. + */ + setForcedVariation(experimentKey: string, userId: string, variationKey: string | null): boolean { + if (!this.validateInputs({ experiment_key: experimentKey, user_id: userId })) { + return false; + } + + const configObj = this.getProjectConfig(); + if (!configObj) { + return false; + } + + try { + return this.decisionService.setForcedVariation(configObj, experimentKey, userId, variationKey); + } catch (ex) { + this.errorReporter.report(ex); + return false; + } + } + + /** + * Gets the forced variation for a given user and experiment. + * @param {string} experimentKey + * @param {string} userId + * @return {string|null} The forced variation key. + */ + getForcedVariation(experimentKey: string, userId: string): string | null { + if (!this.validateInputs({ experiment_key: experimentKey, user_id: userId })) { + return null; + } + + const configObj = this.getProjectConfig(); + if (!configObj) { + return null; + } + + try { + return this.decisionService.getForcedVariation(configObj, experimentKey, userId).result; + } catch (ex) { + this.errorReporter.report(ex); + return null; + } + } + + /** + * Validate string inputs, user attributes and event tags. + * @param {StringInputs} stringInputs Map of string keys and associated values + * @param {unknown} userAttributes Optional parameter for user's attributes + * @param {unknown} eventTags Optional parameter for event tags + * @return {boolean} True if inputs are valid + * + */ + protected validateInputs(stringInputs: StringInputs, userAttributes?: unknown, eventTags?: unknown): boolean { + try { + if (stringInputs.hasOwnProperty('user_id')) { + const userId = stringInputs['user_id']; + if (typeof userId !== 'string' || userId === null || userId === 'undefined') { + throw new OptimizelyError(INVALID_INPUT_FORMAT, 'user_id'); + } + + delete stringInputs['user_id']; + } + Object.keys(stringInputs).forEach(key => { + if (!stringValidator.validate(stringInputs[key as InputKey])) { + throw new OptimizelyError(INVALID_INPUT_FORMAT, key); + } + }); + if (userAttributes) { + validate(userAttributes); + } + if (eventTags) { + eventTagsValidator.validate(eventTags); + } + return true; + } catch (ex) { + this.errorReporter.report(ex); + return false; + } + } + + /** + * Shows failed activation log message and returns null when user is not activated in experiment + * @param {string} experimentKey + * @param {string} userId + * @return {null} + */ + private notActivatingExperiment(experimentKey: string, userId: string): null { + this.logger?.info(NOT_ACTIVATING_USER, userId, experimentKey); + return null; + } + + /** + * Filters out attributes/eventTags with null or undefined values + * @param {EventTags | undefined} map + * @returns {EventTags | undefined} + */ + private filterEmptyValues(map: EventTags | undefined): EventTags | undefined { + for (const key in map) { + const typedKey = key as keyof EventTags; + if (map.hasOwnProperty(typedKey) && (map[typedKey] === null || map[typedKey] === undefined)) { + delete map[typedKey]; + } + } + return map; + } + + /** + * Returns true if the feature is enabled for the given user. + * @param {string} featureKey Key of feature which will be checked + * @param {string} userId ID of user which will be checked + * @param {UserAttributes} attributes Optional user attributes + * @return {boolean} true if the feature is enabled for the user, false otherwise + */ + isFeatureEnabled(featureKey: string, userId: string, attributes?: UserAttributes): boolean { + try { + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'isFeatureEnabled'); + return false; + } + + if (!this.validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { + return false; + } + + const feature = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); + if (!feature) { + return false; + } + + let sourceInfo = {}; + const user = this.createInternalUserContext(userId, attributes) as OptimizelyUserContext; + const decisionObj = this.decisionService.getVariationForFeature(configObj, feature, user).result; + const decisionSource = decisionObj.decisionSource; + const experimentKey = decision.getExperimentKey(decisionObj); + const variationKey = decision.getVariationKey(decisionObj); + + let featureEnabled = decision.getFeatureEnabledFromVariation(decisionObj); + + if (decisionSource === DECISION_SOURCES.FEATURE_TEST || decisionSource === DECISION_SOURCES.HOLDOUT) { + sourceInfo = { + experimentKey: experimentKey, + variationKey: variationKey, + }; + } + + if ( + decisionSource === DECISION_SOURCES.FEATURE_TEST || + decisionSource === DECISION_SOURCES.HOLDOUT || + (decisionSource === DECISION_SOURCES.ROLLOUT && projectConfig.getSendFlagDecisionsValue(configObj)) + ) { + this.sendImpressionEvent(decisionObj, feature.key, userId, featureEnabled, attributes); + } + + if (featureEnabled === true) { + this.logger?.info(FEATURE_ENABLED_FOR_USER, featureKey, userId); + } else { + this.logger?.info(FEATURE_NOT_ENABLED_FOR_USER, featureKey, userId); + featureEnabled = false; + } + + const featureInfo = { + featureKey: featureKey, + featureEnabled: featureEnabled, + source: decisionObj.decisionSource, + sourceInfo: sourceInfo, + }; + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.FEATURE, + userId: userId, + attributes: attributes || {}, + decisionInfo: featureInfo, + }); + + return featureEnabled; + } catch (e) { + this.errorReporter.report(e); + return false; + } + } + + /** + * Returns an Array containing the keys of all features in the project that are + * enabled for the given user. + * @param {string} userId + * @param {UserAttributes} attributes + * @return {string[]} Array of feature keys (strings) + */ + getEnabledFeatures(userId: string, attributes?: UserAttributes): string[] { + try { + const enabledFeatures: string[] = []; + + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getEnabledFeatures'); + return enabledFeatures; + } + + if (!this.validateInputs({ user_id: userId })) { + return enabledFeatures; + } + + objectValues(configObj.featureKeyMap).forEach((feature: FeatureFlag) => { + if (this.isFeatureEnabled(feature.key, userId, attributes)) { + enabledFeatures.push(feature.key); + } + }); + + return enabledFeatures; + } catch (e) { + this.errorReporter.report(e); + return []; + } + } + + /** + * Returns dynamically-typed value of the variable attached to the given + * feature flag. Returns null if the feature key or variable key is invalid. + * + * @param {string} featureKey Key of the feature whose variable's + * value is being accessed + * @param {string} variableKey Key of the variable whose value is + * being accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {unknown} Value of the variable cast to the appropriate + * type, or null if the feature key is invalid or + * the variable key is invalid + */ + getFeatureVariable( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): FeatureVariableValue { + try { + if (!this.getProjectConfig()) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getFeatureVariable'); + return null; + } + return this.getFeatureVariableForType(featureKey, variableKey, null, userId, attributes); + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Helper method to get the value for a variable of a certain type attached to a + * feature flag. Returns null if the feature key is invalid, the variable key is + * invalid, the given variable type does not match the variable's actual type, + * or the variable value cannot be cast to the required type. If the given variable + * type is null, the value of the variable cast to the appropriate type is returned. + * + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string|null} variableType Type of the variable whose value is being + * accessed (must be one of FEATURE_VARIABLE_TYPES + * in lib/utils/enums/index.js), or null to return the + * value of the variable cast to the appropriate type + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {unknown} Value of the variable cast to the appropriate + * type, or null if the feature key is invalid, thevariable + * key is invalid, or there is a mismatch with the type of + * the variable + */ + private getFeatureVariableForType( + featureKey: string, + variableKey: string, + variableType: string | null, + userId: string, + attributes?: UserAttributes + ): FeatureVariableValue { + if (!this.validateInputs({ feature_key: featureKey, variable_key: variableKey, user_id: userId }, attributes)) { + return null; + } + + const configObj = this.getProjectConfig(); + if (!configObj) { + return null; + } + + const featureFlag = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); + if (!featureFlag) { + return null; + } + + const variable = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, this.logger); + if (!variable) { + return null; + } + + if (variableType && variable.type !== variableType) { + this.logger?.warn( + VARIABLE_REQUESTED_WITH_WRONG_TYPE, + variableType, + variable.type + ); + return null; + } + + const user = this.createInternalUserContext(userId, attributes) as OptimizelyUserContext; + const decisionObj = this.decisionService.getVariationForFeature(configObj, featureFlag, user).result; + const featureEnabled = decision.getFeatureEnabledFromVariation(decisionObj); + const variableValue = this.getFeatureVariableValueFromVariation( + featureKey, + featureEnabled, + decisionObj.variation, + variable, + userId + ); + let sourceInfo = {}; + if ( + decisionObj.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decisionObj.experiment !== null && + decisionObj.variation !== null + ) { + sourceInfo = { + experimentKey: decisionObj.experiment.key, + variationKey: decisionObj.variation.key, + }; + } + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.FEATURE_VARIABLE, + userId: userId, + attributes: attributes || {}, + decisionInfo: { + featureKey: featureKey, + featureEnabled: featureEnabled, + source: decisionObj.decisionSource, + variableKey: variableKey, + variableValue: variableValue, + variableType: variable.type, + sourceInfo: sourceInfo, + }, + }); + return variableValue; + } + + /** + * Helper method to get the non type-casted value for a variable attached to a + * feature flag. Returns appropriate variable value depending on whether there + * was a matching variation, feature was enabled or not or varible was part of the + * available variation or not. Also logs the appropriate message explaining how it + * evaluated the value of the variable. + * + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {boolean} featureEnabled Boolean indicating if feature is enabled or not + * @param {Variation} variation variation returned by decision service + * @param {FeatureVariable} variable varible whose value is being evaluated + * @param {string} userId ID for the user + * @return {unknown} Value of the variable or null if the + * config Obj is null + */ + private getFeatureVariableValueFromVariation( + featureKey: string, + featureEnabled: boolean, + variation: Variation | null, + variable: FeatureVariable, + userId: string + ): FeatureVariableValue { + const configObj = this.getProjectConfig(); + if (!configObj) { + return null; + } + + let variableValue = variable.defaultValue; + if (variation !== null) { + const value = projectConfig.getVariableValueForVariation(configObj, variable, variation, this.logger); + if (value !== null) { + if (featureEnabled) { + variableValue = value; + this.logger?.info( + USER_RECEIVED_VARIABLE_VALUE, + variableValue, + variable.key, + featureKey + ); + } else { + this.logger?.info( + FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE, + featureKey, + userId, + variableValue + ); + } + } else { + this.logger?.info( + VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE, + variable.key, + variation.key + ); + } + } else { + this.logger?.info( + USER_RECEIVED_DEFAULT_VARIABLE_VALUE, + userId, + variable.key, + featureKey + ); + } + + return projectConfig.getTypeCastValue(variableValue, variable.type, this.logger); + } + + /** + * Returns value for the given boolean variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {boolean|null} Boolean value of the variable, or null if the + * feature key is invalid, the variable key is invalid, + * or there is a mismatch with the type of the variable. + */ + getFeatureVariableBoolean( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): boolean | null { + try { + if (!this.getProjectConfig()) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getFeatureVariableBoolean'); + return null; + } + return this.getFeatureVariableForType( + featureKey, + variableKey, + FEATURE_VARIABLE_TYPES.BOOLEAN, + userId, + attributes + ) as boolean | null; + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Returns value for the given double variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {number|null} Number value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableDouble( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): number | null { + try { + if (!this.getProjectConfig()) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getFeatureVariableDouble'); + return null; + } + return this.getFeatureVariableForType( + featureKey, + variableKey, + FEATURE_VARIABLE_TYPES.DOUBLE, + userId, + attributes + ) as number | null; + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Returns value for the given integer variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {number|null} Number value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableInteger( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): number | null { + try { + if (!this.getProjectConfig()) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getFeatureVariableInteger'); + return null; + } + return this.getFeatureVariableForType( + featureKey, + variableKey, + FEATURE_VARIABLE_TYPES.INTEGER, + userId, + attributes + ) as number | null; + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Returns value for the given string variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {string|null} String value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableString( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): string | null { + try { + if (!this.getProjectConfig()) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getFeatureVariableString'); + return null; + } + return this.getFeatureVariableForType( + featureKey, + variableKey, + FEATURE_VARIABLE_TYPES.STRING, + userId, + attributes + ) as string | null; + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Returns value for the given json variable attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variable's value is + * being accessed + * @param {string} variableKey Key of the variable whose value is being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {unknown} Object value of the variable, or null if the + * feature key is invalid, the variable key is + * invalid, or there is a mismatch with the type + * of the variable + */ + getFeatureVariableJSON(featureKey: string, variableKey: string, userId: string, attributes: UserAttributes): unknown { + try { + if (!this.getProjectConfig()) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getFeatureVariableJSON'); + return null; + } + return this.getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.JSON, userId, attributes); + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Returns values for all the variables attached to the given feature + * flag. + * @param {string} featureKey Key of the feature whose variables are being + * accessed + * @param {string} userId ID for the user + * @param {UserAttributes} attributes Optional user attributes + * @return {object|null} Object containing all the variables, or null if the + * feature key is invalid + */ + getAllFeatureVariables( + featureKey: string, + userId: string, + attributes?: UserAttributes + ): { [variableKey: string]: unknown } | null { + try { + const configObj = this.getProjectConfig(); + + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'getAllFeatureVariables'); + return null; + } + + if (!this.validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { + return null; + } + + const featureFlag = projectConfig.getFeatureFromKey(configObj, featureKey, this.logger); + if (!featureFlag) { + return null; + } + + const user = this.createInternalUserContext(userId, attributes) as OptimizelyUserContext; + + const decisionObj = this.decisionService.getVariationForFeature(configObj, featureFlag, user).result; + const featureEnabled = decision.getFeatureEnabledFromVariation(decisionObj); + const allVariables: { [variableKey: string]: unknown } = {}; + + featureFlag.variables.forEach((variable: FeatureVariable) => { + allVariables[variable.key] = this.getFeatureVariableValueFromVariation( + featureKey, + featureEnabled, + decisionObj.variation, + variable, + userId + ); + }); + + let sourceInfo = {}; + if ( + decisionObj.decisionSource === DECISION_SOURCES.FEATURE_TEST && + decisionObj.experiment !== null && + decisionObj.variation !== null + ) { + sourceInfo = { + experimentKey: decisionObj.experiment.key, + variationKey: decisionObj.variation.key, + }; + } + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.ALL_FEATURE_VARIABLES, + userId: userId, + attributes: attributes || {}, + decisionInfo: { + featureKey: featureKey, + featureEnabled: featureEnabled, + source: decisionObj.decisionSource, + variableValues: allVariables, + sourceInfo: sourceInfo, + }, + }); + + return allVariables; + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + /** + * Returns OptimizelyConfig object containing experiments and features data + * @return {OptimizelyConfig|null} + * + * OptimizelyConfig Object Schema + * { + * 'experimentsMap': { + * 'my-fist-experiment': { + * 'id': '111111', + * 'key': 'my-fist-experiment' + * 'variationsMap': { + * 'variation_1': { + * 'id': '121212', + * 'key': 'variation_1', + * 'variablesMap': { + * 'age': { + * 'id': '222222', + * 'key': 'age', + * 'type': 'integer', + * 'value': '0', + * } + * } + * } + * } + * } + * }, + * 'featuresMap': { + * 'awesome-feature': { + * 'id': '333333', + * 'key': 'awesome-feature', + * 'experimentsMap': Object, + * 'variationsMap': Object, + * } + * } + * } + */ + getOptimizelyConfig(): OptimizelyConfig | null { + try { + const configObj = this.getProjectConfig(); + if (!configObj) { + return null; + } + return this.projectConfigManager.getOptimizelyConfig() || null; + } catch (e) { + this.errorReporter.report(e); + return null; + } + } + + flushImmediately(): Promise<unknown> { + const flushPromises = []; + + if (!this.isRunning()) { + return Promise.resolve(); + } + + if (this.eventProcessor) { + flushPromises.push(this.eventProcessor.flushImmediately()); + } + + if(this.odpManager) { + flushPromises.push(this.odpManager.flushImmediately()); + } + + return Promise.all(flushPromises); + } + + /** + * Stop background processes belonging to this instance, including: + * + * - Active datafile requests + * - Pending datafile requests + * - Pending event queue flushes + * + * In-flight datafile requests will be aborted. Any events waiting to be sent + * as part of a batched event request will be immediately flushed to the event + * dispatcher. + * + * Returns a Promise that fulfills after all in-flight event dispatcher requests + * (including any final request resulting from flushing the queue as described + * above) are complete. If there are no in-flight event dispatcher requests and + * no queued events waiting to be sent, returns an immediately-fulfilled Promise. + * + * + * NOTE: After close is called, this instance is no longer usable - any events + * generated will no longer be sent to the event dispatcher. + * + * @return {Promise} + */ + close(): Promise<unknown> { + this.stop(); + return this.onTerminated(); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (!this.isRunning()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'Client') + )); + } + + this.state = ServiceState.Stopping; + + this.projectConfigManager.stop(); + this.eventProcessor?.stop(); + this.odpManager?.stop(); + this.notificationCenter.clearAllNotificationListeners(); + + this.cleanupTasks.forEach((onClose) => onClose()); + + Promise.all([ + this.projectConfigManager.onTerminated(), + this.eventProcessor ? this.eventProcessor.onTerminated() : Promise.resolve(), + this.odpManager ? this.odpManager.onTerminated() : Promise.resolve(), + ]).then(() => { + this.state = ServiceState.Terminated; + this.stopPromise.resolve() + }).catch((err) => { + this.errorReporter.report(err); + this.state = ServiceState.Failed; + this.stopPromise.reject(err); + }); + } + + /** + * Returns a Promise that fulfills when this instance is ready to use (meaning + * it has a valid datafile), or rejects when it has failed to become ready within a period of + * time (configurable by the timeout property of the options argument), or when + * this instance is closed via the close method before it became ready. + * + * If a static project config manager with a valid datafile was provided in the constructor, + * the returned Promise is immediately fulfilled. If a polling config manager was provided, + * it will be used to fetch a datafile, and the returned promise will fulfill if that fetch + * succeeds, or it will reject if the datafile fetch does not complete before the timeout. + * The default timeout is 30 seconds. + * + * The returned Promise is fulfilled with an unknown result which is not needed to + * be inspected to know that the instance is ready. If the promise is fulfilled, it + * is guaranteed that the instance is ready to use. If the promise is rejected, it + * means the instance is not ready to use, and the reason for the promise rejection + * will contain an error denoting the cause of failure. + + * @param {Object=} options + * @param {number|undefined} options.timeout + * @return {Promise} + */ + onReady(options?: { timeout?: number }): Promise<unknown> { + let timeoutValue: number | undefined; + if (typeof options === 'object' && options !== null) { + if (options.timeout !== undefined) { + timeoutValue = options.timeout; + } + } + if (!isSafeInteger(timeoutValue)) { + timeoutValue = DEFAULT_ONREADY_TIMEOUT; + } + + const timeoutPromise = resolvablePromise(); + + const cleanupTaskId = this.nextCleanupTaskId++; + + const onReadyTimeout = () => { + this.cleanupTasks.delete(cleanupTaskId); + timeoutPromise.reject(new Error( + sprintf(ONREADY_TIMEOUT, timeoutValue) + )); + }; + + const readyTimeout = setTimeout(onReadyTimeout, timeoutValue); + + this.cleanupTasks.set(cleanupTaskId, () => { + clearTimeout(readyTimeout); + timeoutPromise.reject(new Error(INSTANCE_CLOSED)); + }); + + return Promise.race([this.onRunning().then(() => { + clearTimeout(readyTimeout); + this.cleanupTasks.delete(cleanupTaskId); + }), timeoutPromise]); + } + + //============ decide ============// + + /** + * Creates a context of the user for which decision APIs will be called. + * + * A user context will be created successfully even when the SDK is not fully configured yet, so no + * this.isValidInstance() check is performed here. + * + * @param {string} userId (Optional) The user ID to be used for bucketing. + * @param {UserAttributes} attributes (Optional) user attributes. + * @return {OptimizelyUserContext|null} An OptimizelyUserContext associated with this OptimizelyClient or + * throws if provided inputs are invalid + */ + createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext { + const userIdentifier = userId ?? this.vuidManager?.getVuid(); + + if (userIdentifier === undefined || !this.validateInputs({ user_id: userIdentifier })) { + throw new Error(INVALID_IDENTIFIER); + } + + if (!this.validateInputs({}, attributes)) { + throw new Error(INVALID_ATTRIBUTES); + } + + const userContext = new OptimizelyUserContext({ + optimizely: this, + userId: userIdentifier, + attributes, + }); + + this.onRunning().then(() => { + if (this.odpManager && this.isOdpIntegrated()) { + this.odpManager.identifyUser(userIdentifier); + } + }).catch(() => {}); + + return userContext; + } + + /** + * Creates an internal context of the user for which decision APIs will be called. + * + * A user context will be created successfully even when the SDK is not fully configured yet, so no + * this.isValidInstance() check is performed here. + * + * @param {string} userId The user ID to be used for bucketing. + * @param {UserAttributes} attributes Optional user attributes. + * @return {OptimizelyUserContext|null} An OptimizelyUserContext associated with this OptimizelyClient or + * null if provided inputs are invalid + */ + private createInternalUserContext(userId: string, attributes?: UserAttributes): OptimizelyUserContext | null { + return new OptimizelyUserContext({ + optimizely: this, + userId, + attributes, + }); + } + + decide(user: OptimizelyUserContext, key: string, options: OptimizelyDecideOption[] = []): OptimizelyDecision { + const configObj = this.getProjectConfig(); + + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'decide'); + return newErrorDecision(key, user, [DECISION_MESSAGES.SDK_NOT_READY]); + } + + return this.decideForKeys(user, [key], options, true)[key]; + } + + async decideAsync(user: OptimizelyUserContext, key: string, options: OptimizelyDecideOption[] = []): Promise<OptimizelyDecision> { + const configObj = this.getProjectConfig(); + + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'decide'); + return newErrorDecision(key, user, [DECISION_MESSAGES.SDK_NOT_READY]); + } + + const result = await this.decideForKeysAsync(user, [key], options, true); + return result[key]; + } + + /** + * Get all decide options. + * @param {OptimizelyDecideOption[]} options decide options + * @return {[key: string]: boolean} Map of all provided decide options including default decide options + */ + private getAllDecideOptions(options: OptimizelyDecideOption[]): { [key: string]: boolean } { + const allDecideOptions = { ...this.defaultDecideOptions }; + if (!Array.isArray(options)) { + this.logger?.debug(INVALID_DECIDE_OPTIONS); + } else { + options.forEach(option => { + // Filter out all provided decide options that are not in OptimizelyDecideOption[] + if (OptimizelyDecideOption[option]) { + allDecideOptions[option] = true; + } else { + this.logger?.warn(UNRECOGNIZED_DECIDE_OPTION, option); + } + }); + } + + return allDecideOptions; + } + + /** + * Makes a decision for a given feature key. + * + * @param {OptimizelyUserContext} user - The user context associated with this Optimizely client. + * @param {string} key - The feature key for which a decision will be made. + * @param {DecisionObj} decisionObj - The decision object containing decision details. + * @param {DecisionReasons[]} reasons - An array of reasons for the decision. + * @param {Record<string, boolean>} options - A map of options for decision-making. + * @param {projectConfig.ProjectConfig} configObj - The project configuration object. + * @returns {OptimizelyDecision} - The decision object for the feature flag. + */ + private generateDecision( + user: OptimizelyUserContext, + key: string, + decisionObj: DecisionObj, + reasons: DecisionReasons[], + options: Record<string, boolean>, + configObj: projectConfig.ProjectConfig, + ): OptimizelyDecision { + const userId = user.getUserId() + const attributes = user.getAttributes() + const feature = configObj.featureKeyMap[key] + const decisionSource = decisionObj.decisionSource; + const experimentKey = decisionObj.experiment?.key ?? null; + const experimentId = decisionObj.experiment?.id ?? null; + const variationKey = decisionObj.variation?.key ?? null; + const variationId = decisionObj.variation?.id ?? null; + const flagEnabled: boolean = decision.getFeatureEnabledFromVariation(decisionObj); + const variablesMap: { [key: string]: unknown } = {}; + let decisionEventDispatched = false; + + if (flagEnabled) { + this.logger?.info(FEATURE_ENABLED_FOR_USER, key, userId); + } else { + this.logger?.info(FEATURE_NOT_ENABLED_FOR_USER, key, userId); + } + + if (!options[OptimizelyDecideOption.EXCLUDE_VARIABLES]) { + feature.variables.forEach(variable => { + variablesMap[variable.key] = this.getFeatureVariableValueFromVariation( + key, + flagEnabled, + decisionObj.variation, + variable, + userId + ); + }); + } + + if ( + !options[OptimizelyDecideOption.DISABLE_DECISION_EVENT] && + (decisionSource === DECISION_SOURCES.FEATURE_TEST || + decisionSource === DECISION_SOURCES.HOLDOUT || + (decisionSource === DECISION_SOURCES.ROLLOUT && projectConfig.getSendFlagDecisionsValue(configObj))) + ) { + this.sendImpressionEvent(decisionObj, key, userId, flagEnabled, attributes); + decisionEventDispatched = true; + } + + const shouldIncludeReasons = options[OptimizelyDecideOption.INCLUDE_REASONS]; + + let reportedReasons: string[] = []; + if (shouldIncludeReasons) { + reportedReasons = reasons.map(reason => sprintf(reason[0] as string, ...reason.slice(1))); + } + + const featureInfo = { + flagKey: key, + enabled: flagEnabled, + variationKey: variationKey, + ruleKey: experimentKey, + variables: variablesMap, + reasons: reportedReasons, + decisionEventDispatched: decisionEventDispatched, + experimentId: experimentId, + variationId: variationId, + }; + + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.DECISION, { + type: DECISION_NOTIFICATION_TYPES.FLAG, + userId: userId, + attributes: attributes, + decisionInfo: featureInfo, + }); + + return { + variationKey: variationKey, + enabled: flagEnabled, + variables: variablesMap, + ruleKey: experimentKey, + flagKey: key, + userContext: user, + reasons: reportedReasons, + }; + } + + /** + * Returns an object of decision results for multiple flag keys and a user context. + * If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. + * The SDK will always return an object of decisions. When it cannot process requests, it will return an empty object after logging the errors. + * @param {OptimizelyUserContext} user A user context associated with this OptimizelyClient + * @param {string[]} keys An array of flag keys for which decisions will be made. + * @param {OptimizelyDecideOption[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of decision results mapped by flag keys. + */ + decideForKeys( + user: OptimizelyUserContext, + keys: string[], + options: OptimizelyDecideOption[] = [], + ignoreEnabledFlagOption?:boolean + ): Record<string, OptimizelyDecision> { + return this.getDecisionForKeys('sync', user, keys, options, ignoreEnabledFlagOption).get(); + } + + decideForKeysAsync( + user: OptimizelyUserContext, + keys: string[], + options: OptimizelyDecideOption[] = [], + ignoreEnabledFlagOption?:boolean + ): Promise<Record<string, OptimizelyDecision>> { + return this.getDecisionForKeys('async', user, keys, options, ignoreEnabledFlagOption).get(); + } + + private getDecisionForKeys<OP extends OpType>( + op: OP, + user: OptimizelyUserContext, + keys: string[], + options: OptimizelyDecideOption[] = [], + ignoreEnabledFlagOption?:boolean + ): Value<OP, Record<string, OptimizelyDecision>> { + const decisionMap: Record<string, OptimizelyDecision> = {}; + const flagDecisions: Record<string, DecisionObj> = {}; + const decisionReasonsMap: Record<string, DecisionReasons[]> = {}; + + const configObj = this.getProjectConfig() + + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'decideForKeys'); + return Value.of(op, decisionMap); + } + + if (keys.length === 0) { + return Value.of(op, decisionMap); + } + + const allDecideOptions = this.getAllDecideOptions(options); + + if (ignoreEnabledFlagOption) { + delete allDecideOptions[OptimizelyDecideOption.ENABLED_FLAGS_ONLY]; + } + + const validFlags: FeatureFlag[] = []; + + for(const key of keys) { + const feature = configObj.featureKeyMap[key]; + if (!feature) { + this.logger?.error(FEATURE_NOT_IN_DATAFILE, key); + decisionMap[key] = newErrorDecision(key, user, [sprintf(DECISION_MESSAGES.FLAG_KEY_INVALID, key)]); + continue; + } + + validFlags.push(feature); + } + + return this.decisionService.resolveVariationsForFeatureList(op, configObj, validFlags, user, allDecideOptions) + .then((decisionList) => { + for(let i = 0; i < validFlags.length; i++) { + const key = validFlags[i].key; + const decision = decisionList[i]; + + if(decision.error) { + decisionMap[key] = newErrorDecision(key, user, decision.reasons.map(r => sprintf(r[0], ...r.slice(1)))); + } else { + flagDecisions[key] = decision.result; + decisionReasonsMap[key] = decision.reasons; + } + } + + for(const validFlag of validFlags) { + const validKey = validFlag.key; + + // if there is already a value for this flag, that must have come from + // the newErrorDecision above, so we skip it + if (decisionMap[validKey]) { + continue; + } + + const decision = this.generateDecision(user, validKey, flagDecisions[validKey], decisionReasonsMap[validKey], allDecideOptions, configObj); + + if(!allDecideOptions[OptimizelyDecideOption.ENABLED_FLAGS_ONLY] || decision.enabled) { + decisionMap[validKey] = decision; + } + } + + return Value.of(op, decisionMap); + }, + ); + } + + /** + * Returns an object of decision results for all active flag keys. + * @param {OptimizelyUserContext} user A user context associated with this OptimizelyClient + * @param {OptimizelyDecideOption[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of all decision results mapped by flag keys. + */ + decideAll( + user: OptimizelyUserContext, + options: OptimizelyDecideOption[] = [] + ): { [key: string]: OptimizelyDecision } { + const decisionMap: { [key: string]: OptimizelyDecision } = {}; + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'decideAll'); + return decisionMap; + } + + const allFlagKeys = Object.keys(configObj.featureKeyMap); + + return this.decideForKeys(user, allFlagKeys, options); + } + + async decideAllAsync( + user: OptimizelyUserContext, + options: OptimizelyDecideOption[] = [] + ): Promise<Record<string, OptimizelyDecision>> { + const decisionMap: { [key: string]: OptimizelyDecision } = {}; + const configObj = this.getProjectConfig(); + if (!configObj) { + this.errorReporter.report(NO_PROJECT_CONFIG_FAILURE, 'decideAll'); + return decisionMap; + } + + const allFlagKeys = Object.keys(configObj.featureKeyMap); + + return this.decideForKeysAsync(user, allFlagKeys, options); + } + + /** + * Updates ODP Config with most recent ODP key, host, pixelUrl, and segments from the project config + */ + private updateOdpSettings(): void { + const projectConfig = this.getProjectConfig(); + + if (!projectConfig) { + return; + } + + if (this.odpManager) { + this.odpManager.updateConfig(projectConfig.odpIntegrationConfig); + } + } + + /** + * Sends an action as an ODP Event with optional custom parameters including type, identifiers, and data + * Note: Since this depends on this.odpManager, it must await Optimizely client's onReady() promise resolution. + * @param {string} action Subcategory of the event type (i.e. "client_initialized", "identified", or a custom action) + * @param {string} type (Optional) Type of event (Defaults to "fullstack") + * @param {Map<string, string>} identifiers (Optional) Key-value map of user identifiers + * @param {Map<string, string>} data (Optional) Event data in a key-value map. + */ + public sendOdpEvent( + action: string, + type?: string, + identifiers?: Map<string, string>, + data?: Map<string, unknown> + ): void { + if (!this.odpManager) { + this.logger?.error(ODP_EVENT_FAILED_ODP_MANAGER_MISSING); + return; + } + + try { + const odpEvent = new OdpEvent(type || '', action, identifiers, data); + this.odpManager.sendEvent(odpEvent); + } catch (e) { + this.logger?.error(ODP_EVENT_FAILED, e); + } + } + /** + * Checks if ODP (Optimizely Data Platform) is integrated into the project. + * @returns { boolean } `true` if ODP settings were found in the datafile otherwise `false` + */ + public isOdpIntegrated(): boolean { + return this.getProjectConfig()?.odpIntegrationConfig?.integrated ?? false; + } + + /** + * Fetches list of qualified segments from ODP for a particular userId. + * @param {string} userId + * @param {Array<OptimizelySegmentOption>} options + * @returns {Promise<string[] | null>} + */ + public async fetchQualifiedSegments( + userId: string, + options?: Array<OptimizelySegmentOption> + ): Promise<string[] | null> { + if (!this.odpManager) { + return null; + } + + return await this.odpManager.fetchQualifiedSegments(userId, options); + } + + /** + * @returns {string|undefined} Currently provisioned VUID from local ODP Manager or undefined if + * ODP Manager has not been instantiated yet for any reason. + */ + public getVuid(): string | undefined { + if (!this.vuidManager) { + this.logger?.error(UNABLE_TO_GET_VUID_VUID_MANAGER_NOT_AVAILABLE); + return undefined; + } + + return this.vuidManager.getVuid(); + } +} diff --git a/lib/optimizely_decision/index.ts b/lib/optimizely_decision/index.ts new file mode 100644 index 000000000..b4adaed14 --- /dev/null +++ b/lib/optimizely_decision/index.ts @@ -0,0 +1,28 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import { OptimizelyUserContext, OptimizelyDecision } from '../shared_types'; + +export function newErrorDecision(key: string, user: OptimizelyUserContext, reasons: string[]): OptimizelyDecision { + return { + variationKey: null, + enabled: false, + variables: {}, + ruleKey: null, + flagKey: key, + userContext: user, + reasons: reasons, + }; +} diff --git a/lib/optimizely_user_context/index.tests.js b/lib/optimizely_user_context/index.tests.js new file mode 100644 index 000000000..9f3597187 --- /dev/null +++ b/lib/optimizely_user_context/index.tests.js @@ -0,0 +1,1110 @@ +/** + * Copyright 2020-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; +import sinon from 'sinon'; +import { sprintf } from '../utils/fns'; +import { NOTIFICATION_TYPES } from '../notification_center/type'; +import OptimizelyUserContext from './'; +import { createNotificationCenter } from '../notification_center'; +import Optimizely from '../optimizely'; +import { LOG_LEVEL } from '../utils/enums'; +import testData from '../tests/test_data'; +import { OptimizelyDecideOption } from '../shared_types'; +import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; +import { createProjectConfig } from '../project_config/project_config'; +import { getForwardingEventProcessor } from '../event_processor/event_processor_factory'; +import { FORCED_DECISION_NULL_RULE_KEY } from './index' + +import { + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED_BUT_INVALID, + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED, + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID, +} from '../core/decision_service'; + +const getMockEventDispatcher = () => { + const dispatcher = { + dispatchEvent: sinon.spy(() => Promise.resolve({ statusCode: 200 })), + } + return dispatcher; +} + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}); + +const getOptlyInstance = ({ datafileObj, defaultDecideOptions }) => { + const createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + const mockConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(datafileObj), + }); + const eventDispatcher = getMockEventDispatcher(); + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + + const optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: mockConfigManager, + eventProcessor, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: defaultDecideOptions || [], + }); + + + return { optlyInstance, eventProcessor, eventDispatcher, createdLogger } +} + +describe('lib/optimizely_user_context', function() { + describe('APIs', function() { + var fakeOptimizely; + var userId = 'tester'; + var options = ['fakeOption']; + describe('#setAttribute', function() { + fakeOptimizely = { + decide: sinon.stub().returns({}), + }; + it('should set attributes when provided at instantiation of OptimizelyUserContext', function() { + var userId = 'user1'; + var attributes = { test_attribute: 'test_value' }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + attributes, + }); + user.setAttribute('k1', { hello: 'there' }); + user.setAttribute('k2', true); + user.setAttribute('k3', 100); + user.setAttribute('k4', 3.5); + assert.deepEqual(user.getOptimizely(), fakeOptimizely); + assert.deepEqual(user.getUserId(), userId); + + var newAttributes = user.getAttributes(); + assert.deepEqual(newAttributes['test_attribute'], 'test_value'); + assert.deepEqual(newAttributes['k1'], { hello: 'there' }); + assert.deepEqual(newAttributes['k2'], true); + assert.deepEqual(newAttributes['k3'], 100); + assert.deepEqual(newAttributes['k4'], 3.5); + }); + + it('should set attributes when none provided at instantiation of OptimizelyUserContext', function() { + var userId = 'user2'; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + user.setAttribute('k1', { hello: 'there' }); + user.setAttribute('k2', true); + user.setAttribute('k3', 100); + user.setAttribute('k4', 3.5); + assert.deepEqual(user.getOptimizely(), fakeOptimizely); + assert.deepEqual(user.getUserId(), userId); + + var newAttributes = user.getAttributes(); + assert.deepEqual(newAttributes['k1'], { hello: 'there' }); + assert.deepEqual(newAttributes['k2'], true); + assert.deepEqual(newAttributes['k3'], 100); + assert.deepEqual(newAttributes['k4'], 3.5); + }); + + it('should override existing attributes', function() { + var userId = 'user3'; + var attributes = { test_attribute: 'test_value' }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + attributes, + }); + user.setAttribute('k1', { hello: 'there' }); + user.setAttribute('test_attribute', 'overwritten_value'); + assert.deepEqual(user.getOptimizely(), fakeOptimizely); + assert.deepEqual(user.getUserId(), userId); + + var newAttributes = user.getAttributes(); + assert.deepEqual(newAttributes['k1'], { hello: 'there' }); + assert.deepEqual(newAttributes['test_attribute'], 'overwritten_value'); + assert.deepEqual(Object.keys(newAttributes).length, 2); + }); + + it('should allow to set attributes with value of null', function() { + var userId = 'user4'; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + user.setAttribute('null_attribute', null); + assert.deepEqual(user.getOptimizely(), fakeOptimizely); + assert.deepEqual(user.getUserId(), userId); + + var newAttributes = user.getAttributes(); + assert.deepEqual(newAttributes['null_attribute'], null); + }); + + it('should set attributes by value in constructor', function() { + var userId = 'user1'; + var attributes = { initial_attribute: 'initial_value' }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + attributes, + }); + attributes['attribute2'] = 100; + assert.deepEqual(user.getAttributes(), { initial_attribute: 'initial_value' }); + user.setAttribute('attribute3', 'hello'); + assert.deepEqual(attributes, { initial_attribute: 'initial_value', attribute2: 100 }); + }); + + it('should not change user attributes if returned by getAttributes object is updated', function() { + var userId = 'user1'; + var attributes = { initial_attribute: 'initial_value' }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + attributes, + }); + var attributes2 = user.getAttributes(); + attributes2['new_attribute'] = { value: 100 }; + assert.deepEqual(user.getAttributes(), attributes); + var expectedAttributes = { + initial_attribute: 'initial_value', + new_attribute: { value: 100 }, + }; + assert.deepEqual(attributes2, expectedAttributes); + }); + }); + + describe('#decide', function() { + it('should return an expected decision object', function() { + var flagKey = 'feature_1'; + var fakeDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: 'fakeUserContext', + reasons: [], + }; + fakeOptimizely = { + decide: sinon.stub().returns(fakeDecision), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + var decision = user.decide(flagKey, options); + sinon.assert.calledWithExactly(fakeOptimizely.decide, user, flagKey, options); + assert.deepEqual(decision, fakeDecision); + }); + }); + + describe('#decideForKeys', function() { + it('should return an expected decision results object', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var fakeDecisionMap = { + flagKey1: { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: 'fakeUserContext', + reasons: [], + }, + flagKey2: { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: 'fakeUserContext', + reasons: [], + }, + }; + fakeOptimizely = { + decideForKeys: sinon.stub().returns(fakeDecisionMap), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + var decisionMap = user.decideForKeys([flagKey1, flagKey2], options); + sinon.assert.calledWithExactly(fakeOptimizely.decideForKeys, user, [flagKey1, flagKey2], options); + assert.deepEqual(decisionMap, fakeDecisionMap); + }); + }); + + describe('#decideAll', function() { + it('should return an expected decision results object', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var flagKey3 = 'feature_3'; + var fakeDecisionMap = { + flagKey1: { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: 'fakeUserContext', + reasons: [], + }, + flagKey2: { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: 'fakeUserContext', + reasons: [], + }, + flagKey3: { + variationKey: '', + enabled: false, + variables: {}, + ruleKey: '', + flagKey: flagKey3, + userContext: user, + reasons: [], + }, + }; + fakeOptimizely = { + decideAll: sinon.stub().returns(fakeDecisionMap), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + var decisionMap = user.decideAll(options); + sinon.assert.calledWithExactly(fakeOptimizely.decideAll, user, options); + assert.deepEqual(decisionMap, fakeDecisionMap); + }); + }); + + describe('#trackEvent', function() { + it('should call track from optimizely client', function() { + fakeOptimizely = { + track: sinon.stub(), + }; + var eventName = 'myEvent'; + var eventTags = { eventTag1: 1000 }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + user.trackEvent(eventName, eventTags); + sinon.assert.calledWithExactly( + fakeOptimizely.track, + eventName, + user.getUserId(), + user.getAttributes(), + eventTags + ); + }); + }); + + describe('#setForcedDecision', function() { + let createdLogger = createLogger({ + logLevel: LOG_LEVEL.DEBUG, + logToConsole: false, + }); + + let optlyInstance, eventDispatcher; + + beforeEach(function() { + ({ optlyInstance, createdLogger, eventDispatcher} = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + })); + }); + + afterEach(function() { + eventDispatcher.dispatchEvent.reset(); + }); + + it('should return true when client is not ready', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(false), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + var result = user.setForcedDecision({ flagKey: 'feature_1' }, '3324490562'); + assert.strictEqual(result, true); + }); + + it('should return true when provided empty string flagKey', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(true), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId: 'user123', + }); + var result = user.setForcedDecision({ flagKey: '' }, '3324490562'); + assert.strictEqual(result, true); + }); + + it('should return true when provided flagKey and variationKey', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(true), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId: 'user123', + }); + var result = user.setForcedDecision({ flagKey: 'feature_1' }, '3324490562'); + assert.strictEqual(result, true); + }); + + describe('when valid forced decision is set', function() { + var optlyInstance; + var notificationCenter = createNotificationCenter({ logger: createdLogger }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + }), + eventProcessor, + isValidInstance: true, + logger: createdLogger, + notificationCenter, + }); + + sinon.stub(optlyInstance.decisionService.logger, 'log'); + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + }); + + afterEach(function() { + // optlyInstance.decisionService.logger.log.restore(); + eventDispatcher.dispatchEvent.reset(); + optlyInstance.notificationCenter.sendNotifications.restore(); + }); + + it('should return an expected decision object when forced decision is called and variation of different experiment but same flag key', function() { + var flagKey = 'feature_1'; + var ruleKey = 'exp_with_audience'; + var variationKey = '3324490633'; + + var user = optlyInstance.createUserContext(userId); + user.setForcedDecision({ flagKey: flagKey, ruleKey }, { variationKey }); + var decision = user.decide(flagKey, options); + + assert.equal(decision.variationKey, variationKey); + assert.equal(decision.ruleKey, ruleKey); + assert.equal(decision.enabled, true); + assert.equal(decision.userContext.getUserId(), userId); + assert.deepEqual(decision.userContext.getAttributes(), {}); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + }); + + it('should return forced decision object when forced decision is set for a flag and do NOT dispatch an event with DISABLE_DECISION_EVENT passed in decide options', function() { + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var variationKey = '3324490562'; + user.setForcedDecision({ flagKey: featureKey }, { variationKey }); + var decision = user.decide(featureKey, [ + OptimizelyDecideOption.INCLUDE_REASONS, + OptimizelyDecideOption.DISABLE_DECISION_EVENT, + ]); + assert.equal(decision.variationKey, variationKey); + assert.equal(decision.ruleKey, null); + assert.equal(decision.enabled, true); + assert.equal(decision.userContext.getUserId(), userId); + assert.deepEqual(decision.userContext.getAttributes(), {}); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual( + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], + { variationKey } + ); + assert.equal( + true, + decision.reasons.includes( + sprintf(USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, variationKey, featureKey, userId) + ) + ); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('should return forced decision object when forced decision is set for a flag and do NOT dispatch an event with DISABLE_DECISION_EVENT string passed in decide options', function() { + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var variationKey = '3324490562'; + user.setForcedDecision({ flagKey: featureKey }, { variationKey }); + var decision = user.decide(featureKey, ['INCLUDE_REASONS', 'DISABLE_DECISION_EVENT']); + assert.equal(decision.variationKey, variationKey); + assert.equal(decision.ruleKey, null); + assert.equal(decision.enabled, true); + assert.equal(decision.userContext.getUserId(), userId); + assert.deepEqual(decision.userContext.getAttributes(), {}); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual( + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], + { variationKey } + ); + assert.equal( + true, + decision.reasons.includes( + sprintf(USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, variationKey, featureKey, userId) + ) + ); + + sinon.assert.notCalled(eventDispatcher.dispatchEvent); + }); + + it('should return forced decision object when forced decision is set for a flag and dispatch an event', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var variationKey = '3324490562'; + user.setForcedDecision({ flagKey: featureKey }, { variationKey }); + var decision = user.decide(featureKey, [OptimizelyDecideOption.INCLUDE_REASONS]); + + assert.equal(decision.variationKey, variationKey); + assert.equal(decision.ruleKey, null); + assert.equal(decision.enabled, true); + assert.equal(decision.userContext.getUserId(), userId); + assert.deepEqual(decision.userContext.getAttributes(), {}); + assert.deepEqual(Object.values(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual( + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], + { variationKey } + ); + assert.equal( + true, + decision.reasons.includes( + sprintf(USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, variationKey, featureKey, userId) + ) + ); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + var impressionEvent = callArgs[0]; + var eventDecision = impressionEvent.params.visitors[0].snapshots[0].decisions[0]; + var metadata = eventDecision.metadata; + + assert.equal(eventDecision.experiment_id, ''); + assert.equal(eventDecision.variation_id, '3324490562'); + + assert.equal(metadata.flag_key, featureKey); + assert.equal(metadata.rule_key, ''); + assert.equal(metadata.rule_type, 'feature-test'); + assert.equal(metadata.variation_key, variationKey); + assert.equal(metadata.enabled, true); + + sinon.assert.callCount(notificationCenter.sendNotifications, 3); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(2).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: featureKey, + enabled: true, + ruleKey: null, + variationKey, + variables: { + b_true: true, + d_4_2: 4.2, + i_1: 'invalid', + i_42: 42, + j_1: null, + s_foo: 'foo', + }, + decisionEventDispatched: true, + reasons: [ + sprintf( + USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED, + variationKey, + featureKey, + userId + ), + ], + experimentId: null, + variationId: '3324490562' + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should return forced decision object when forced decision is set for an experiment rule and dispatch an event', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var attributes = { country: 'US' }; + var user = optlyInstance.createUserContext(userId, attributes); + var featureKey = 'feature_1'; + var variationKey = 'b'; + var ruleKey = 'exp_with_audience'; + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey }); + var decision = user.decide(featureKey, [OptimizelyDecideOption.INCLUDE_REASONS]); + + assert.equal(decision.variationKey, variationKey); + assert.equal(decision.ruleKey, ruleKey); + assert.equal(decision.enabled, false); + assert.equal(decision.userContext.getUserId(), userId); + assert.deepEqual(decision.userContext.getAttributes(), attributes); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap[featureKey]).length, 1); + assert.deepEqual(decision.userContext.forcedDecisionsMap[featureKey][ruleKey], { variationKey }); + assert.equal( + true, + decision.reasons.includes( + sprintf( + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED, + variationKey, + featureKey, + ruleKey, + userId + ) + ) + ); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + var impressionEvent = callArgs[0]; + var eventDecision = impressionEvent.params.visitors[0].snapshots[0].decisions[0]; + var metadata = eventDecision.metadata; + + assert.equal(eventDecision.experiment_id, '10390977673'); + assert.equal(eventDecision.variation_id, '10416523121'); + + assert.equal(metadata.flag_key, featureKey); + assert.equal(metadata.rule_key, 'exp_with_audience'); + assert.equal(metadata.rule_type, 'feature-test'); + assert.equal(metadata.variation_key, 'b'); + assert.equal(metadata.enabled, false); + + sinon.assert.callCount(notificationCenter.sendNotifications, 3); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(2).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes, + decisionInfo: { + flagKey: featureKey, + enabled: false, + ruleKey: 'exp_with_audience', + variationKey, + variables: { + b_true: true, + d_4_2: 4.2, + i_1: 'invalid', + i_42: 42, + j_1: null, + s_foo: 'foo', + }, + decisionEventDispatched: true, + reasons: [ + sprintf( + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED, + variationKey, + featureKey, + ruleKey, + userId + ), + ], + experimentId: '10390977673', + variationId: '10416523121', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + + it('should return forced decision object when forced decision is set for a delivery rule and dispatch an event', function() { + const { optlyInstance, eventDispatcher } = getOptlyInstance({ + datafileObj: testData.getTestDecideProjectConfig(), + }); + + const notificationCenter = optlyInstance.notificationCenter; + sinon.stub(notificationCenter, 'sendNotifications'); + + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var variationKey = '3324490633'; + var ruleKey = '3332020515'; + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey }); + var decision = user.decide(featureKey); + + assert.equal(decision.variationKey, variationKey); + assert.equal(decision.ruleKey, ruleKey); + assert.equal(decision.enabled, true); + assert.equal(decision.userContext.getUserId(), userId); + assert.deepEqual(decision.userContext.getAttributes(), {}); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap[featureKey]).length, 1); + assert.deepEqual(decision.userContext.forcedDecisionsMap[featureKey][ruleKey], { variationKey }); + + // sinon.assert.called(stubLogHandler.log); + // var logMessage = optlyInstance.decisionService.logger.log.args[4]; + // assert.strictEqual(logMessage[0], 2); + // assert.strictEqual( + // logMessage[1], + // 'Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.' + // ); + // assert.strictEqual(logMessage[2], variationKey); + // assert.strictEqual(logMessage[3], featureKey); + // assert.strictEqual(logMessage[4], ruleKey); + // assert.strictEqual(logMessage[5], userId); + + sinon.assert.calledOnce(eventDispatcher.dispatchEvent); + var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; + var impressionEvent = callArgs[0]; + var eventDecision = impressionEvent.params.visitors[0].snapshots[0].decisions[0]; + var metadata = eventDecision.metadata; + + assert.equal(eventDecision.experiment_id, '3332020515'); + assert.equal(eventDecision.variation_id, '3324490633'); + + assert.equal(metadata.flag_key, featureKey); + assert.equal(metadata.rule_key, '3332020515'); + assert.equal(metadata.rule_type, 'rollout'); + assert.equal(metadata.variation_key, '3324490633'); + assert.equal(metadata.enabled, true); + + sinon.assert.callCount(notificationCenter.sendNotifications, 3); + var notificationCallArgs = notificationCenter.sendNotifications.getCall(2).args; + var expectedNotificationCallArgs = [ + NOTIFICATION_TYPES.DECISION, + { + type: 'flag', + userId: 'tester', + attributes: {}, + decisionInfo: { + flagKey: featureKey, + enabled: true, + ruleKey: '3332020515', + variationKey, + variables: { + b_true: true, + d_4_2: 4.2, + i_1: 'invalid', + i_42: 42, + j_1: null, + s_foo: 'foo', + }, + decisionEventDispatched: true, + reasons: [], + experimentId: '3332020515', + variationId: '3324490633', + }, + }, + ]; + assert.deepEqual(notificationCallArgs, expectedNotificationCallArgs); + }); + }); + + describe('when invalid forced decision is set', function() { + var optlyInstance; + var notificationCenter = createNotificationCenter({ logger: createdLogger }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + }), + eventProcessor, + isValidInstance: true, + logger: createdLogger, + notificationCenter, + }); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + + it('should NOT return forced decision object when forced decision is set for a flag', function() { + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var variationKey = 'invalid'; + user.setForcedDecision({ flagKey: featureKey }, { variationKey }); + var decision = user.decide(featureKey, [OptimizelyDecideOption.INCLUDE_REASONS]); + + // invalid forced decision will be ignored and regular decision will return + assert.equal(decision.variationKey, '18257766532'); + assert.equal(decision.ruleKey, '18322080788'); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual( + decision.userContext.forcedDecisionsMap[featureKey][FORCED_DECISION_NULL_RULE_KEY], + { variationKey } + ); + assert.equal( + true, + decision.reasons.includes( + sprintf(USER_HAS_FORCED_DECISION_WITH_NO_RULE_SPECIFIED_BUT_INVALID, featureKey, userId) + ) + ); + }); + + it('should NOT return forced decision object when forced decision is set for an experiment rule', function() { + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var ruleKey = 'exp_with_audience'; + var variationKey = 'invalid'; + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey }); + var decision = user.decide(featureKey, [OptimizelyDecideOption.INCLUDE_REASONS]); + + // invalid forced-decision will be ignored and regular decision will return + assert.equal(decision.variationKey, '18257766532'); + assert.equal(decision.ruleKey, '18322080788'); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap[featureKey]).length, 1); + assert.deepEqual(decision.userContext.forcedDecisionsMap[featureKey][ruleKey], { variationKey }); + assert.equal( + true, + decision.reasons.includes( + sprintf( + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID, + featureKey, + ruleKey, + userId + ) + ) + ); + }); + + it('should NOT return forced decision object when forced decision is set for a delivery rule', function() { + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var variationKey = 'invalid'; + var ruleKey = '3332020515'; + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey }); + var decision = user.decide(featureKey, [OptimizelyDecideOption.INCLUDE_REASONS]); + + // invalid forced decision will be ignored and regular decision will return + assert.equal(decision.variationKey, '18257766532'); + assert.equal(decision.ruleKey, '18322080788'); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap[featureKey]).length, 1); + assert.deepEqual(decision.userContext.forcedDecisionsMap[featureKey][ruleKey], { variationKey }); + assert.equal( + true, + decision.reasons.includes( + sprintf( + USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID, + featureKey, + ruleKey, + userId + ) + ) + ); + }); + }); + + describe('when forced decision is set for a flag and an experiment rule', function() { + var optlyInstance; + const createdLogger = createLogger({ + logLevel: LOG_LEVEL.DEBUG, + logToConsole: false, + }); + var notificationCenter = createNotificationCenter({ logger: createdLogger }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + }), + eventProcessor, + isValidInstance: true, + logger: createdLogger, + notificationCenter, + }); + }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }); + + it('should prioritize flag forced decision over experiment rule', function() { + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var flagVariationKey = '3324490562'; + var experimentRuleVariationKey = 'b'; + var ruleKey = 'exp_with_audience'; + user.setForcedDecision({ flagKey: featureKey }, { variationKey: flagVariationKey }); + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey: experimentRuleVariationKey }); + var decision = user.decide(featureKey, [OptimizelyDecideOption.INCLUDE_REASONS]); + + // flag-to-decision is the 1st priority + assert.equal(decision.variationKey, flagVariationKey); + assert.equal(decision.ruleKey, null); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap).length, 1); + assert.deepEqual(Object.keys(decision.userContext.forcedDecisionsMap[featureKey]).length, 2); + }); + }); + }); + + describe('#getForcedDecision', function() { + it('should return correct forced variation', function() { + const createdLogger = createLogger({ + logLevel: LOG_LEVEL.DEBUG, + logToConsole: false, + }); + var notificationCenter = createNotificationCenter({ logger: createdLogger }); + var eventDispatcher = getMockEventDispatcher(); + var eventProcessor = getForwardingEventProcessor( + eventDispatcher, + ); + var optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestDecideProjectConfig()) + }), + eventProcessor, + isValidInstance: true, + logger: createdLogger, + notificationCenter, + }); + var user = optlyInstance.createUserContext(userId); + var featureKey = 'feature_1'; + var ruleKey = 'r'; + user.setForcedDecision({ flagKey: featureKey }, { variationKey: 'fv1' }); + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey }), { variationKey: 'fv1' }); + + // override forced variation + user.setForcedDecision({ flagKey: featureKey }, { variationKey: 'fv2' }); + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey }), { variationKey: 'fv2' }); + + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey: 'ev1' }); + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey, ruleKey }), { variationKey: 'ev1' }); + + // override forced variation + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey: 'ev2' }); + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey, ruleKey }), { variationKey: 'ev2' }); + + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey }), { variationKey: 'fv2' }); + }); + }); + + describe('#removeForcedDecision', function() { + it('should return true when client is not ready and the forced decision has been removed successfully', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(false), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId: 'user123', + }); + user.setForcedDecision({ flagKey: 'feature_1' }, '3324490562'); + var result = user.removeForcedDecision({ flagKey: 'feature_1' }); + assert.strictEqual(result, true); + }); + + it('should return true when the forced decision has been removed successfully and false otherwise', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(true), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId: 'user123', + }); + user.setForcedDecision({ flagKey: 'feature_1' }, '3324490562'); + var result1 = user.removeForcedDecision({ flagKey: 'feature_1' }); + assert.strictEqual(result1, true); + + var result2 = user.removeForcedDecision('non-existent_feature'); + assert.strictEqual(result2, false); + }); + + it('should successfully remove forced decision when multiple forced decisions set with same feature key', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(true), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId: 'user123', + }); + + var featureKey = 'feature_1'; + var ruleKey = 'r'; + + user.setForcedDecision({ flagKey: featureKey }, { variationKey: 'fv1' }); + user.setForcedDecision({ flagKey: featureKey, ruleKey }, { variationKey: 'ev1' }); + + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey }), { variationKey: 'fv1' }); + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey, ruleKey }), { variationKey: 'ev1' }); + + assert.strictEqual(user.removeForcedDecision({ flagKey: featureKey }), true); + assert.strictEqual(user.getForcedDecision({ flagKey: featureKey }), null); + assert.deepEqual(user.getForcedDecision({ flagKey: featureKey, ruleKey }), { variationKey: 'ev1' }); + + assert.strictEqual(user.removeForcedDecision({ flagKey: featureKey, ruleKey }), true); + assert.strictEqual(user.getForcedDecision({ flagKey: featureKey }), null); + assert.strictEqual(user.getForcedDecision({ flagKey: featureKey, ruleKey }), null); + + assert.strictEqual(user.removeForcedDecision({ flagKey: featureKey }), false); // no more saved decisions + }); + }); + + describe('#removeAllForcedDecisions', function() { + it('should return true when client is not ready', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(false), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + var result = user.removeAllForcedDecisions(); + assert.strictEqual(result, true); + }); + + it('should return true when all forced decisions have been removed successfully', function() { + fakeOptimizely = { + isValidInstance: sinon.stub().returns(true), + }; + var user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + user.setForcedDecision({ flagKey: 'feature_1' }, { variationKey: '3324490562' }); + user.setForcedDecision({ flagKey: 'feature_1', ruleKey: 'exp_with_audience' }, { variationKey: 'b' }); + assert.deepEqual(Object.keys(user.forcedDecisionsMap).length, 1); + assert.deepEqual(Object.keys(user.forcedDecisionsMap['feature_1']).length, 2); + + assert.deepEqual(user.getForcedDecision({ flagKey: 'feature_1' }), { variationKey: '3324490562' }); + assert.deepEqual(user.getForcedDecision({ flagKey: 'feature_1', ruleKey: 'exp_with_audience' }), { + variationKey: 'b', + }); + + var result1 = user.removeAllForcedDecisions(); + assert.strictEqual(result1, true); + assert.deepEqual(Object.keys(user.forcedDecisionsMap).length, 0); + + assert.strictEqual(user.getForcedDecision({ flagKey: 'feature_1' }), null); + assert.strictEqual(user.getForcedDecision({ flagKey: 'feature_1', ruleKey: 'exp_with_audience' }), null); + }); + }); + + describe('fetchQualifiedSegments', () => { + it('should successfully get segments', async () => { + fakeOptimizely = { + fetchQualifiedSegments: sinon.stub().returns(['a']), + }; + const user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + + const successfullyFetched = await user.fetchQualifiedSegments(); + assert.deepEqual(successfullyFetched, true); + + sinon.assert.calledWithExactly(fakeOptimizely.fetchQualifiedSegments, userId, undefined); + + assert.deepEqual(user.qualifiedSegments, ['a']); + }); + + it('should return true empty returned segements', async () => { + fakeOptimizely = { + fetchQualifiedSegments: sinon.stub().returns([]), + }; + const user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + + const successfullyFetched = await user.fetchQualifiedSegments(); + assert.deepEqual(successfullyFetched, true); + }); + + it('should return false in other cases', async () => { + fakeOptimizely = { + fetchQualifiedSegments: sinon.stub().returns(null), + }; + const user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + + const successfullyFetched = await user.fetchQualifiedSegments(); + assert.deepEqual(successfullyFetched, false); + }); + }); + + describe('isQualifiedFor', () => { + it('should successfully return the expected result for if a user is qualified for a particular segment or not', () => { + const user = new OptimizelyUserContext({ + shouldIdentifyUser: false, + optimizely: fakeOptimizely, + userId, + }); + + user.qualifiedSegments = ['a', 'b']; + + assert.deepEqual(user.isQualifiedFor('a'), true); + assert.deepEqual(user.isQualifiedFor('b'), true); + assert.deepEqual(user.isQualifiedFor('c'), false); + }); + }); + }); +}); diff --git a/lib/optimizely_user_context/index.ts b/lib/optimizely_user_context/index.ts new file mode 100644 index 000000000..7b2af6488 --- /dev/null +++ b/lib/optimizely_user_context/index.ts @@ -0,0 +1,297 @@ +/** + * Copyright 2020-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Optimizely from '../optimizely'; +import { + EventTags, + OptimizelyDecideOption, + OptimizelyDecision, + OptimizelyDecisionContext, + OptimizelyForcedDecision, + UserAttributeValue, + UserAttributes, +} from '../shared_types'; +import { OptimizelySegmentOption } from '../odp/segment_manager/optimizely_segment_option'; + +export const FORCED_DECISION_NULL_RULE_KEY = '$opt_null_rule_key'; + +interface OptimizelyUserContextConfig { + optimizely: Optimizely; + userId: string; + attributes?: UserAttributes; +} + +export interface IOptimizelyUserContext { + qualifiedSegments: string[] | null; + getUserId(): string; + getAttributes(): UserAttributes; + setAttribute(key: string, value: unknown): void; + decide(key: string, options?: OptimizelyDecideOption[]): OptimizelyDecision; + decideAsync(key: string, options?: OptimizelyDecideOption[]): Promise<OptimizelyDecision>; + decideForKeys(keys: string[], options?: OptimizelyDecideOption[]): { [key: string]: OptimizelyDecision }; + decideForKeysAsync(keys: string[], options?: OptimizelyDecideOption[]): Promise<Record<string, OptimizelyDecision>>; + decideAll(options?: OptimizelyDecideOption[]): { [key: string]: OptimizelyDecision }; + decideAllAsync(options?: OptimizelyDecideOption[]): Promise<Record<string, OptimizelyDecision>>; + trackEvent(eventName: string, eventTags?: EventTags): void; + setForcedDecision(context: OptimizelyDecisionContext, decision: OptimizelyForcedDecision): boolean; + getForcedDecision(context: OptimizelyDecisionContext): OptimizelyForcedDecision | null; + removeForcedDecision(context: OptimizelyDecisionContext): boolean; + removeAllForcedDecisions(): boolean; + fetchQualifiedSegments(options?: OptimizelySegmentOption[]): Promise<boolean>; + isQualifiedFor(segment: string): boolean; +} + +export default class OptimizelyUserContext implements IOptimizelyUserContext { + private optimizely: Optimizely; + private userId: string; + private attributes: UserAttributes; + private forcedDecisionsMap: { [key: string]: { [key: string]: OptimizelyForcedDecision } }; + private _qualifiedSegments: string[] | null = null; + + constructor({ optimizely, userId, attributes }: OptimizelyUserContextConfig) { + this.optimizely = optimizely; + this.userId = userId; + this.attributes = { ...attributes }; + this.forcedDecisionsMap = {}; + } + + /** + * Sets an attribute for a given key. + * @param {string} key An attribute key + * @param {any} value An attribute value + */ + setAttribute(key: string, value: UserAttributeValue): void { + this.attributes[key] = value; + } + + getUserId(): string { + return this.userId; + } + + getAttributes(): UserAttributes { + return { ...this.attributes }; + } + + getOptimizely(): Optimizely { + return this.optimizely; + } + + public get qualifiedSegments(): string[] | null { + return this._qualifiedSegments; + } + + public set qualifiedSegments(qualifiedSegments: string[] | null) { + this._qualifiedSegments = qualifiedSegments; + } + + /** + * Returns a decision result for a given flag key and a user context, which contains all data required to deliver the flag. + * If the SDK finds an error, it will return a decision with null for variationKey. The decision will include an error message in reasons. + * @param {string} key A flag key for which a decision will be made. + * @param {OptimizelyDecideOption} options An array of options for decision-making. + * @return {OptimizelyDecision} A decision result. + */ + decide(key: string, options: OptimizelyDecideOption[] = []): OptimizelyDecision { + return this.optimizely.decide(this.cloneUserContext(), key, options); + } + + /** + * Returns a promise that resolves in decision result for a given flag key and a user context, which contains all data required to deliver the flag. + * If the SDK finds an error, it will return a decision with null for variationKey. The decision will include an error message in reasons. + * @param {string} key A flag key for which a decision will be made. + * @param {OptimizelyDecideOption} options An array of options for decision-making. + * @return {Promise<OptimizelyDecision>} A Promise that resolves decision result. + */ + decideAsync(key: string, options?: OptimizelyDecideOption[]): Promise<OptimizelyDecision> { + return this.optimizely.decideAsync(this.cloneUserContext(), key, options); + } + + /** + * Returns an object of decision results for multiple flag keys and a user context. + * If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. + * The SDK will always return key-mapped decisions. When it cannot process requests, it will return an empty map after logging the errors. + * @param {string[]} keys An array of flag keys for which decisions will be made. + * @param {OptimizelyDecideOption[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of decision results mapped by flag keys. + */ + decideForKeys(keys: string[], options: OptimizelyDecideOption[] = []): { [key: string]: OptimizelyDecision } { + return this.optimizely.decideForKeys(this.cloneUserContext(), keys, options); + } + + /** + * Returns a promise that resolves in an object of decision results for multiple flag keys and a user context. + * If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. + * The SDK will always return key-mapped decisions. When it cannot process requests, it will return an empty map after logging the errors. + * @param {string[]} keys An array of flag keys for which decisions will be made. + * @param {OptimizelyDecideOption[]} options An array of options for decision-making. + * @return {Promise<Record<string, OptimizelyDecision>>} A promise that resolves in an object of decision results mapped by flag keys. + */ + decideForKeysAsync(keys: string[], options?: OptimizelyDecideOption[]): Promise<Record<string, OptimizelyDecision>> { + return this.optimizely.decideForKeysAsync(this.cloneUserContext(), keys, options); + } + /** + * Returns an object of decision results for all active flag keys. + * @param {OptimizelyDecideOption[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of all decision results mapped by flag keys. + */ + decideAll(options: OptimizelyDecideOption[] = []): { [key: string]: OptimizelyDecision } { + return this.optimizely.decideAll(this.cloneUserContext(), options); + } + + /** + * Returns a promise that resolves in an object of decision results for all active flag keys. + * @param {OptimizelyDecideOption[]} options An array of options for decision-making. + * @return {Promise<Record<string ,OptimizelyDecision>>} A promise that resolves in an object of all decision results mapped by flag keys. + */ + decideAllAsync(options: OptimizelyDecideOption[] = []): Promise<Record<string, OptimizelyDecision>> { + return this.optimizely.decideAllAsync(this.cloneUserContext(), options); + } + + /** + * Tracks an event. + * @param {string} eventName The event name. + * @param {EventTags} eventTags An optional map of event tag names to event tag values. + */ + trackEvent(eventName: string, eventTags?: EventTags): void { + this.optimizely.track(eventName, this.userId, this.attributes, eventTags); + } + + /** + * Sets the forced decision for specified optimizely decision context. + * @param {OptimizelyDecisionContext} context OptimizelyDecisionContext containing flagKey and optional ruleKey. + * @param {OptimizelyForcedDecision} decision OptimizelyForcedDecision containing forced variation key. + * @return {boolean} true if the forced decision has been set successfully. + */ + setForcedDecision(context: OptimizelyDecisionContext, decision: OptimizelyForcedDecision): boolean { + const flagKey = context.flagKey; + + const ruleKey = context.ruleKey ?? FORCED_DECISION_NULL_RULE_KEY; + const variationKey = decision.variationKey; + const forcedDecision = { variationKey }; + + if (!this.forcedDecisionsMap[flagKey]) { + this.forcedDecisionsMap[flagKey] = {}; + } + this.forcedDecisionsMap[flagKey][ruleKey] = forcedDecision; + + return true; + } + + /** + * Returns the forced decision for specified optimizely decision context. + * @param {OptimizelyDecisionContext} context OptimizelyDecisionContext containing flagKey and optional ruleKey. + * @return {OptimizelyForcedDecision|null} OptimizelyForcedDecision for specified context if exists or null. + */ + getForcedDecision(context: OptimizelyDecisionContext): OptimizelyForcedDecision | null { + return this.findForcedDecision(context); + } + + /** + * Removes the forced decision for specified optimizely decision context. + * @param {OptimizelyDecisionContext} context OptimizelyDecisionContext containing flagKey and optional ruleKey. + * @return {boolean} true if the forced decision has been removed successfully + */ + removeForcedDecision(context: OptimizelyDecisionContext): boolean { + const ruleKey = context.ruleKey ?? FORCED_DECISION_NULL_RULE_KEY; + const flagKey = context.flagKey; + + let isForcedDecisionRemoved = false; + + if (this.forcedDecisionsMap.hasOwnProperty(flagKey)) { + const forcedDecisionByRuleKey = this.forcedDecisionsMap[flagKey]; + if (forcedDecisionByRuleKey.hasOwnProperty(ruleKey)) { + delete this.forcedDecisionsMap[flagKey][ruleKey]; + isForcedDecisionRemoved = true; + } + if (Object.keys(this.forcedDecisionsMap[flagKey]).length === 0) { + delete this.forcedDecisionsMap[flagKey]; + } + } + + return isForcedDecisionRemoved; + } + + /** + * Removes all forced decisions bound to this user context. + * @return {boolean} true if the forced decision has been removed successfully + */ + removeAllForcedDecisions(): boolean { + this.forcedDecisionsMap = {}; + return true; + } + + /** + * Finds a forced decision in forcedDecisionsMap for provided optimizely decision context. + * @param {OptimizelyDecisionContext} context OptimizelyDecisionContext containing flagKey and optional ruleKey. + * @return {OptimizelyForcedDecision|null} OptimizelyForcedDecision for specified context if exists or null. + */ + private findForcedDecision(context: OptimizelyDecisionContext): OptimizelyForcedDecision | null { + let variationKey; + const validRuleKey = context.ruleKey ?? FORCED_DECISION_NULL_RULE_KEY; + const flagKey = context.flagKey; + + if (this.forcedDecisionsMap.hasOwnProperty(context.flagKey)) { + const forcedDecisionByRuleKey = this.forcedDecisionsMap[flagKey]; + if (forcedDecisionByRuleKey.hasOwnProperty(validRuleKey)) { + variationKey = forcedDecisionByRuleKey[validRuleKey].variationKey; + return { variationKey }; + } + } + + return null; + } + + private cloneUserContext(): OptimizelyUserContext { + const userContext = new OptimizelyUserContext({ + optimizely: this.getOptimizely(), + userId: this.getUserId(), + attributes: this.getAttributes(), + }); + + if (Object.keys(this.forcedDecisionsMap).length > 0) { + userContext.forcedDecisionsMap = { ...this.forcedDecisionsMap }; + } + + userContext._qualifiedSegments = this._qualifiedSegments; + + return userContext; + } + + /** + * Fetches a target user's list of qualified segments filtered by any given segment options and stores in qualifiedSegments. + * @param {OptimizelySegmentOption[]} options (Optional) List of segment options used to filter qualified segment results. + * @returns Boolean representing if segments were populated. + */ + async fetchQualifiedSegments(options?: OptimizelySegmentOption[]): Promise<boolean> { + const segments = await this.optimizely.fetchQualifiedSegments(this.userId, options); + + this.qualifiedSegments = segments; + + return segments !== null; + } + + /** + * Returns a boolean representing if a user is qualified for a particular segment. + * @param {string} segment Target segment to be evaluated for user qualification. + * @returns {boolean} Boolean representing if a user qualified for the passed in segment. + */ + isQualifiedFor(segment: string): boolean { + if (!this._qualifiedSegments) { + return false; + } + + return this._qualifiedSegments.indexOf(segment) > -1; + } +} diff --git a/lib/project_config/config_manager_factory.browser.spec.ts b/lib/project_config/config_manager_factory.browser.spec.ts new file mode 100644 index 000000000..9dfa7bced --- /dev/null +++ b/lib/project_config/config_manager_factory.browser.spec.ts @@ -0,0 +1,87 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('./config_manager_factory', () => { + return { + getPollingConfigManager: vi.fn().mockReturnValueOnce({ foo: 'bar' }), + getOpaquePollingConfigManager: vi.fn().mockReturnValueOnce({ foo: 'bar' }), + }; +}); + +vi.mock('../utils/http_request_handler/request_handler.browser', () => { + const BrowserRequestHandler = vi.fn(); + return { BrowserRequestHandler }; +}); + +import { getOpaquePollingConfigManager, PollingConfigManagerConfig, PollingConfigManagerFactoryOptions } from './config_manager_factory'; +import { createPollingProjectConfigManager } from './config_manager_factory.browser'; +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; +import { getMockSyncCache } from '../tests/mock/mock_cache'; + +describe('createPollingConfigManager', () => { + const mockGetOpaquePollingConfigManager = vi.mocked(getOpaquePollingConfigManager); + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + + beforeEach(() => { + mockGetOpaquePollingConfigManager.mockClear(); + MockBrowserRequestHandler.mockClear(); + }); + + it('creates and returns the instance by calling getPollingConfigManager', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(Object.is(projectConfigManager, mockGetOpaquePollingConfigManager.mock.results[0].value)).toBe(true); + }); + + it('uses an instance of BrowserRequestHandler as requestHandler', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(Object.is(mockGetOpaquePollingConfigManager.mock.calls[0][0].requestHandler, MockBrowserRequestHandler.mock.instances[0])).toBe(true); + }); + + it('uses uses autoUpdate = false by default', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager.mock.calls[0][0].autoUpdate).toBe(false); + }); + + it('uses the provided options', () => { + const config: PollingConfigManagerConfig = { + datafile: '{}', + jsonSchemaValidator: vi.fn(), + sdkKey: 'sdkKey', + updateInterval: 50000, + autoUpdate: true, + urlTemplate: 'urlTemplate', + datafileAccessToken: 'datafileAccessToken', + cache: getMockSyncCache<string>(), + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); + }); +}); diff --git a/lib/project_config/config_manager_factory.browser.ts b/lib/project_config/config_manager_factory.browser.ts new file mode 100644 index 000000000..0a96affd5 --- /dev/null +++ b/lib/project_config/config_manager_factory.browser.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; +import { ProjectConfigManager } from './project_config_manager'; + +export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { + const defaultConfig = { + autoUpdate: false, + requestHandler: new BrowserRequestHandler(), + }; + return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); +}; diff --git a/lib/project_config/config_manager_factory.node.spec.ts b/lib/project_config/config_manager_factory.node.spec.ts new file mode 100644 index 000000000..c0631a63b --- /dev/null +++ b/lib/project_config/config_manager_factory.node.spec.ts @@ -0,0 +1,87 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('./config_manager_factory', () => { + return { + getPollingConfigManager: vi.fn().mockReturnValueOnce({ foo: 'bar' }), + getOpaquePollingConfigManager: vi.fn().mockReturnValueOnce({ foo: 'bar' }), + }; +}); + +vi.mock('../utils/http_request_handler/request_handler.node', () => { + const NodeRequestHandler = vi.fn(); + return { NodeRequestHandler }; +}); + +import { getOpaquePollingConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; +import { createPollingProjectConfigManager } from './config_manager_factory.node'; +import { NodeRequestHandler } from '../utils/http_request_handler/request_handler.node'; +import { getMockSyncCache } from '../tests/mock/mock_cache'; + +describe('createPollingConfigManager', () => { + const mockGetOpaquePollingConfigManager = vi.mocked(getOpaquePollingConfigManager); + const MockNodeRequestHandler = vi.mocked(NodeRequestHandler); + + beforeEach(() => { + mockGetOpaquePollingConfigManager.mockClear(); + MockNodeRequestHandler.mockClear(); + }); + + it('creates and returns the instance by calling getPollingConfigManager', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(Object.is(projectConfigManager, mockGetOpaquePollingConfigManager.mock.results[0].value)).toBe(true); + }); + + it('uses an instance of NodeRequestHandler as requestHandler', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(Object.is(mockGetOpaquePollingConfigManager.mock.calls[0][0].requestHandler, MockNodeRequestHandler.mock.instances[0])).toBe(true); + }); + + it('uses uses autoUpdate = true by default', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager.mock.calls[0][0].autoUpdate).toBe(true); + }); + + it('uses the provided options', () => { + const config: PollingConfigManagerConfig = { + datafile: '{}', + jsonSchemaValidator: vi.fn(), + sdkKey: 'sdkKey', + updateInterval: 50000, + autoUpdate: false, + urlTemplate: 'urlTemplate', + datafileAccessToken: 'datafileAccessToken', + cache: getMockSyncCache(), + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); + }); +}); diff --git a/lib/project_config/config_manager_factory.node.ts b/lib/project_config/config_manager_factory.node.ts new file mode 100644 index 000000000..58ac126bc --- /dev/null +++ b/lib/project_config/config_manager_factory.node.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; +import { NodeRequestHandler } from "../utils/http_request_handler/request_handler.node"; +import { ProjectConfigManager } from "./project_config_manager"; +import { DEFAULT_URL_TEMPLATE, DEFAULT_AUTHENTICATED_URL_TEMPLATE } from './constant'; + +export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { + const defaultConfig = { + autoUpdate: true, + requestHandler: new NodeRequestHandler(), + }; + return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); +}; diff --git a/lib/project_config/config_manager_factory.react_native.spec.ts b/lib/project_config/config_manager_factory.react_native.spec.ts new file mode 100644 index 000000000..52411861d --- /dev/null +++ b/lib/project_config/config_manager_factory.react_native.spec.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +await vi.hoisted(async () => { + await mockRequireAsyncStorage(); +}); + +let isAsyncStorageAvailable = true; + +async function mockRequireAsyncStorage() { + const { Module } = await import('module'); + const M: any = Module; + + M._load_original = M._load; + M._load = (uri: string, parent: string) => { + if (uri === '@react-native-async-storage/async-storage') { + if (isAsyncStorageAvailable) return { default: {} }; + throw new Error("Module not found: @react-native-async-storage/async-storage"); + } + return M._load_original(uri, parent); + }; +} + +vi.mock('./config_manager_factory', () => { + return { + getPollingConfigManager: vi.fn().mockReturnValueOnce({ foo: 'bar' }), + getOpaquePollingConfigManager: vi.fn().mockRejectedValueOnce({ foo: 'bar' }), + }; +}); + +vi.mock('../utils/http_request_handler/request_handler.browser', () => { + const BrowserRequestHandler = vi.fn(); + return { BrowserRequestHandler }; +}); + +vi.mock('../utils/cache/async_storage_cache.react_native', async (importOriginal) => { + const original: any = await importOriginal(); + const OriginalAsyncStorageCache = original.AsyncStorageCache; + const MockAsyncStorageCache = vi.fn().mockImplementation(function (this: any, ...args) { + Object.setPrototypeOf(this, new OriginalAsyncStorageCache(...args)); + }); + return { AsyncStorageCache: MockAsyncStorageCache }; +}); + +import { getOpaquePollingConfigManager, getPollingConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; +import { createPollingProjectConfigManager } from './config_manager_factory.react_native'; +import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; +import { getMockSyncCache } from '../tests/mock/mock_cache'; + +describe('createPollingConfigManager', () => { + const mockGetOpaquePollingConfigManager = vi.mocked(getOpaquePollingConfigManager); + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const MockAsyncStorageCache = vi.mocked(AsyncStorageCache); + + beforeEach(() => { + mockGetOpaquePollingConfigManager.mockClear(); + MockBrowserRequestHandler.mockClear(); + MockAsyncStorageCache.mockClear(); + }); + + it('creates and returns the instance by calling getPollingConfigManager', () => { + const config = { + sdkKey: 'sdkKey', + }; + + const projectConfigManager = createPollingProjectConfigManager(config); + expect(Object.is(projectConfigManager, mockGetOpaquePollingConfigManager.mock.results[0].value)).toBe(true); + }); + + it('uses an instance of BrowserRequestHandler as requestHandler', () => { + const config = { + sdkKey: 'sdkKey', + }; + + createPollingProjectConfigManager(config); + + expect( + Object.is( + mockGetOpaquePollingConfigManager.mock.calls[0][0].requestHandler, + MockBrowserRequestHandler.mock.instances[0] + ) + ).toBe(true); + }); + + it('uses uses autoUpdate = true by default', () => { + const config = { + sdkKey: 'sdkKey', + }; + + createPollingProjectConfigManager(config); + + expect(mockGetOpaquePollingConfigManager.mock.calls[0][0].autoUpdate).toBe(true); + }); + + it('uses an instance of ReactNativeAsyncStorageCache for caching by default', () => { + const config = { + sdkKey: 'sdkKey', + }; + + createPollingProjectConfigManager(config); + + expect( + Object.is(mockGetOpaquePollingConfigManager.mock.calls[0][0].cache, MockAsyncStorageCache.mock.instances[0]) + ).toBe(true); + }); + + it('uses the provided options', () => { + const config: PollingConfigManagerConfig = { + datafile: '{}', + jsonSchemaValidator: vi.fn(), + sdkKey: 'sdkKey', + updateInterval: 50000, + autoUpdate: false, + urlTemplate: 'urlTemplate', + datafileAccessToken: 'datafileAccessToken', + cache: getMockSyncCache(), + }; + + createPollingProjectConfigManager(config); + + expect(mockGetOpaquePollingConfigManager).toHaveBeenNthCalledWith(1, expect.objectContaining(config)); + }); + + it('Should not throw error if a cache is present in the config, and async storage is not available', async () => { + isAsyncStorageAvailable = false; + const config = { + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: getMockSyncCache<string>(), + }; + + expect(() => createPollingProjectConfigManager(config)).not.toThrow(); + isAsyncStorageAvailable = true; + }); + + it('should throw an error if cache is not present in the config, and async storage is not available', async () => { + isAsyncStorageAvailable = false; + + const config = { + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + }; + + expect(() => createPollingProjectConfigManager(config)).toThrowError( + "Module not found: @react-native-async-storage/async-storage" + ); + isAsyncStorageAvailable = true; + }); +}); diff --git a/lib/project_config/config_manager_factory.react_native.ts b/lib/project_config/config_manager_factory.react_native.ts new file mode 100644 index 000000000..8ea480595 --- /dev/null +++ b/lib/project_config/config_manager_factory.react_native.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getOpaquePollingConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; +import { BrowserRequestHandler } from "../utils/http_request_handler/request_handler.browser"; +import { ProjectConfigManager } from "./project_config_manager"; +import { AsyncStorageCache } from "../utils/cache/async_storage_cache.react_native"; + +import { OpaqueConfigManager } from "./config_manager_factory"; + +export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { + const defaultConfig = { + autoUpdate: true, + requestHandler: new BrowserRequestHandler(), + cache: config.cache || new AsyncStorageCache(), + }; + + return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); +}; diff --git a/lib/project_config/config_manager_factory.spec.ts b/lib/project_config/config_manager_factory.spec.ts new file mode 100644 index 000000000..7def4f9a8 --- /dev/null +++ b/lib/project_config/config_manager_factory.spec.ts @@ -0,0 +1,184 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('./project_config_manager', () => { + const MockConfigManager = vi.fn(); + return { ProjectConfigManagerImpl: MockConfigManager }; +}); + +vi.mock('./polling_datafile_manager', () => { + const MockDatafileManager = vi.fn(); + return { PollingDatafileManager: MockDatafileManager }; +}); + +vi.mock('../utils/repeater/repeater', () => { + const MockIntervalRepeater = vi.fn(); + const MockExponentialBackoff = vi.fn(); + return { IntervalRepeater: MockIntervalRepeater, ExponentialBackoff: MockExponentialBackoff }; +}); + +import { ProjectConfigManagerImpl } from './project_config_manager'; +import { PollingDatafileManager } from './polling_datafile_manager'; +import { ExponentialBackoff, IntervalRepeater } from '../utils/repeater/repeater'; +import { getPollingConfigManager } from './config_manager_factory'; +import { DEFAULT_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './constant'; +import { getMockSyncCache } from '../tests/mock/mock_cache'; +import { LogLevel } from '../logging/logger'; + +describe('getPollingConfigManager', () => { + const MockProjectConfigManagerImpl = vi.mocked(ProjectConfigManagerImpl); + const MockPollingDatafileManager = vi.mocked(PollingDatafileManager); + const MockIntervalRepeater = vi.mocked(IntervalRepeater); + const MockExponentialBackoff = vi.mocked(ExponentialBackoff); + + beforeEach(() => { + MockProjectConfigManagerImpl.mockClear(); + MockPollingDatafileManager.mockClear(); + MockIntervalRepeater.mockClear(); + MockExponentialBackoff.mockClear(); + }); + + it('should throw an error if the passed cache is not valid', () => { + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: 1 as any, + })).toThrow('Invalid store'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: 'abc' as any, + })).toThrow('Invalid store'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: {} as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: 'abc', get: 'abc', remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method set, Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + const noop = () => {}; + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: noop, get: 'abc', remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method get, Invalid store method remove, Invalid store method getKeys'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: noop, get: noop, remove: 'abc', getKeys: 'abc' } as any, + })).toThrow('Invalid store method remove, Invalid store method getKeys'); + + expect(() => getPollingConfigManager({ + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + cache: { set: noop, get: noop, remove: noop, getKeys: 'abc' } as any, + })).toThrow('Invalid store method getKeys'); + }); + + it('uses a repeater with exponential backoff for the datafileManager', () => { + const config = { + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + }; + const projectConfigManager = getPollingConfigManager(config); + expect(Object.is(projectConfigManager, MockProjectConfigManagerImpl.mock.instances[0])).toBe(true); + const usedDatafileManager = MockProjectConfigManagerImpl.mock.calls[0][0].datafileManager; + expect(Object.is(usedDatafileManager, MockPollingDatafileManager.mock.instances[0])).toBe(true); + const usedRepeater = MockPollingDatafileManager.mock.calls[0][0].repeater; + expect(Object.is(usedRepeater, MockIntervalRepeater.mock.instances[0])).toBe(true); + const usedBackoff = MockIntervalRepeater.mock.calls[0][1]; + expect(Object.is(usedBackoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + }); + + it('uses the default update interval if not provided', () => { + const config = { + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + }; + getPollingConfigManager(config); + expect(MockIntervalRepeater.mock.calls[0][0]).toBe(DEFAULT_UPDATE_INTERVAL); + }); + + it('adds a startup log if the update interval is below the minimum', () => { + const config = { + sdkKey: 'abcd', + requestHandler: { makeRequest: vi.fn() }, + updateInterval: 10000, + }; + getPollingConfigManager(config); + const startupLogs = MockPollingDatafileManager.mock.calls[0][0].startupLogs; + expect(startupLogs).toEqual(expect.arrayContaining([{ + level: LogLevel.Warn, + message: UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE, + params: [], + }])); + }); + + it('does not add any startup log if the update interval above the minimum', () => { + const config = { + sdkKey: 'abcd', + requestHandler: { makeRequest: vi.fn() }, + updateInterval: 40000, + }; + getPollingConfigManager(config); + const startupLogs = MockPollingDatafileManager.mock.calls[0][0].startupLogs; + expect(startupLogs).toEqual([]); + }); + + it('uses the provided options', () => { + const config = { + datafile: '{}', + jsonSchemaValidator: vi.fn(), + sdkKey: 'sdkKey', + requestHandler: { makeRequest: vi.fn() }, + updateInterval: 50000, + autoUpdate: true, + urlTemplate: 'urlTemplate', + datafileAccessToken: 'datafileAccessToken', + cache: getMockSyncCache<string>(), + }; + + getPollingConfigManager(config); + expect(MockIntervalRepeater.mock.calls[0][0]).toBe(config.updateInterval); + expect(MockExponentialBackoff).toHaveBeenNthCalledWith(1, 1000, config.updateInterval, 500); + + expect(MockPollingDatafileManager).toHaveBeenNthCalledWith(1, expect.objectContaining({ + sdkKey: config.sdkKey, + autoUpdate: config.autoUpdate, + urlTemplate: config.urlTemplate, + datafileAccessToken: config.datafileAccessToken, + requestHandler: config.requestHandler, + repeater: MockIntervalRepeater.mock.instances[0], + cache: config.cache, + })); + + expect(MockProjectConfigManagerImpl).toHaveBeenNthCalledWith(1, expect.objectContaining({ + datafile: config.datafile, + jsonSchemaValidator: config.jsonSchemaValidator, + })); + }); +}); diff --git a/lib/project_config/config_manager_factory.ts b/lib/project_config/config_manager_factory.ts new file mode 100644 index 000000000..e7d21aeea --- /dev/null +++ b/lib/project_config/config_manager_factory.ts @@ -0,0 +1,129 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestHandler } from "../utils/http_request_handler/http"; +import { Maybe, Transformer } from "../utils/type"; +import { DatafileManagerConfig } from "./datafile_manager"; +import { ProjectConfigManagerImpl, ProjectConfigManager } from "./project_config_manager"; +import { PollingDatafileManager } from "./polling_datafile_manager"; +import { DEFAULT_UPDATE_INTERVAL } from './constant'; +import { ExponentialBackoff, IntervalRepeater } from "../utils/repeater/repeater"; +import { StartupLog } from "../service"; +import { MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './constant'; +import { LogLevel } from '../logging/logger' +import { Store } from "../utils/cache/store"; +import { validateStore } from "../utils/cache/store_validator"; + +export const INVALID_CONFIG_MANAGER = "Invalid config manager"; + +const configManagerSymbol: unique symbol = Symbol(); + +export type OpaqueConfigManager = { + [configManagerSymbol]: unknown; +}; + +export type StaticConfigManagerConfig = { + datafile: string, + jsonSchemaValidator?: Transformer<unknown, boolean>, +}; + +export const createStaticProjectConfigManager = ( + config: StaticConfigManagerConfig +): OpaqueConfigManager => { + return { + [configManagerSymbol]: new ProjectConfigManagerImpl(config), + } +}; + +export type PollingConfigManagerConfig = { + datafile?: string, + sdkKey: string, + jsonSchemaValidator?: Transformer<unknown, boolean>, + autoUpdate?: boolean; + updateInterval?: number; + urlTemplate?: string; + datafileAccessToken?: string; + cache?: Store<string>; +}; + +export type PollingConfigManagerFactoryOptions = PollingConfigManagerConfig & { requestHandler: RequestHandler }; + +export const getPollingConfigManager = ( + opt: PollingConfigManagerFactoryOptions +): ProjectConfigManager => { + if (opt.cache) { + validateStore(opt.cache); + } + + const updateInterval = opt.updateInterval ?? DEFAULT_UPDATE_INTERVAL; + + const backoff = new ExponentialBackoff(1000, updateInterval, 500); + const repeater = new IntervalRepeater(updateInterval, backoff); + + const startupLogs: StartupLog[] = [] + + if (updateInterval < MIN_UPDATE_INTERVAL) { + startupLogs.push({ + level: LogLevel.Warn, + message: UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE, + params: [], + }); + } + + const datafileManagerConfig: DatafileManagerConfig = { + sdkKey: opt.sdkKey, + autoUpdate: opt.autoUpdate, + urlTemplate: opt.urlTemplate, + datafileAccessToken: opt.datafileAccessToken, + requestHandler: opt.requestHandler, + cache: opt.cache, + repeater, + startupLogs, + }; + + const datafileManager = new PollingDatafileManager(datafileManagerConfig); + + return new ProjectConfigManagerImpl({ + datafile: opt.datafile, + datafileManager, + jsonSchemaValidator: opt.jsonSchemaValidator, + }); +}; + +export const getOpaquePollingConfigManager = (opt: PollingConfigManagerFactoryOptions): OpaqueConfigManager => { + return { + [configManagerSymbol]: getPollingConfigManager(opt), + }; +}; + +export const wrapConfigManager = (configManager: ProjectConfigManager): OpaqueConfigManager => { + return { + [configManagerSymbol]: configManager, + }; +}; + +export const extractConfigManager = (opaqueConfigManager: OpaqueConfigManager): ProjectConfigManager => { + if (!opaqueConfigManager || typeof opaqueConfigManager !== 'object') { + throw new Error(INVALID_CONFIG_MANAGER); + } + + const configManager = opaqueConfigManager[configManagerSymbol]; + if (!configManager) { + throw new Error(INVALID_CONFIG_MANAGER); + } + + return opaqueConfigManager[configManagerSymbol] as ProjectConfigManager; +}; diff --git a/lib/project_config/config_manager_factory.universal.ts b/lib/project_config/config_manager_factory.universal.ts new file mode 100644 index 000000000..bcc664082 --- /dev/null +++ b/lib/project_config/config_manager_factory.universal.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; +import { RequestHandler } from "../utils/http_request_handler/http"; +import { validateRequestHandler } from "../utils/http_request_handler/request_handler_validator"; + +export type UniversalPollingConfigManagerConfig = PollingConfigManagerConfig & { + requestHandler: RequestHandler; +} + +export const createPollingProjectConfigManager = (config: UniversalPollingConfigManagerConfig): OpaqueConfigManager => { + validateRequestHandler(config.requestHandler); + const defaultConfig = { + autoUpdate: true, + }; + return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); +}; diff --git a/lib/project_config/constant.ts b/lib/project_config/constant.ts new file mode 100644 index 000000000..55e69a33e --- /dev/null +++ b/lib/project_config/constant.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2022-2023, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const DEFAULT_UPDATE_INTERVAL_MINUTES = 5; +/** Standard interval (5 minutes in milliseconds) for polling datafile updates.; */ +export const DEFAULT_UPDATE_INTERVAL = DEFAULT_UPDATE_INTERVAL_MINUTES * 60 * 1000; + +const MIN_UPDATE_INTERVAL_SECONDS = 30; +/** Minimum allowed interval (30 seconds in milliseconds) for polling datafile updates. */ +export const MIN_UPDATE_INTERVAL = MIN_UPDATE_INTERVAL_SECONDS * 1000; + +export const UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE = `Polling intervals below ${MIN_UPDATE_INTERVAL_SECONDS} seconds are not recommended.`; + +export const DEFAULT_URL_TEMPLATE = `https://cdn.optimizely.com/datafiles/%s.json`; + +export const DEFAULT_AUTHENTICATED_URL_TEMPLATE = `https://config.optimizely.com/datafiles/auth/%s.json`; + +export const BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT = [0, 8, 16, 32, 64, 128, 256, 512]; + +export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute diff --git a/lib/project_config/datafile_manager.ts b/lib/project_config/datafile_manager.ts new file mode 100644 index 000000000..c5765a539 --- /dev/null +++ b/lib/project_config/datafile_manager.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Service, StartupLog } from '../service'; +import { Store } from '../utils/cache/store'; +import { RequestHandler } from '../utils/http_request_handler/http'; +import { Fn, Consumer } from '../utils/type'; +import { Repeater } from '../utils/repeater/repeater'; +import { LoggerFacade } from '../logging/logger'; + +export interface DatafileManager extends Service { + get(): string | undefined; + onUpdate(listener: Consumer<string>): Fn; + setLogger(logger: LoggerFacade): void; +} + +export type DatafileManagerConfig = { + requestHandler: RequestHandler; + autoUpdate?: boolean; + sdkKey: string; + urlTemplate?: string; + cache?: Store<string>; + datafileAccessToken?: string; + initRetry?: number; + repeater: Repeater; + logger?: LoggerFacade; + startupLogs?: StartupLog[]; +} diff --git a/lib/project_config/optimizely_config.spec.ts b/lib/project_config/optimizely_config.spec.ts new file mode 100644 index 000000000..6e9e6747b --- /dev/null +++ b/lib/project_config/optimizely_config.spec.ts @@ -0,0 +1,937 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach, vi, assert } from 'vitest'; +import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; +import { createProjectConfig, ProjectConfig } from './project_config'; +import { + getTestProjectConfigWithFeatures, + getTypedAudiencesConfig, + getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig, + getDuplicateExperimentKeyConfig, +} from '../tests/test_data'; +import { Experiment } from '../shared_types'; +import { LoggerFacade } from '../logging/logger'; +import { getMockLogger } from '../tests/mock/mock_logger'; + +const datafile: ProjectConfig = getTestProjectConfigWithFeatures(); +const typedAudienceDatafile = getTypedAudiencesConfig(); +const similarRuleKeyDatafile = getSimilarRuleKeyConfig(); +const similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); +const cloneDeep = (obj: any) => JSON.parse(JSON.stringify(obj)); +const getAllExperimentsFromDatafile = (datafile: ProjectConfig) => { + const allExperiments: Experiment[] = []; + datafile.groups.forEach(group => { + group.experiments.forEach(experiment => { + allExperiments.push(experiment); + }); + }); + datafile.experiments.forEach(experiment => { + allExperiments.push(experiment); + }); + return allExperiments; +}; + +describe('Optimizely Config', () => { + let optimizelyConfigObject: OptimizelyConfig; + let projectConfigObject: ProjectConfig; + let projectTypedAudienceConfigObject: ProjectConfig; + let optimizelySimilarRuleKeyConfigObject: OptimizelyConfig; + let projectSimilarRuleKeyConfigObject: ProjectConfig; + let optimizelySimilarExperimentkeyConfigObject: OptimizelyConfig; + let projectSimilarExperimentKeyConfigObject: ProjectConfig; + + const logger = getMockLogger(); + + beforeEach(() => { + projectConfigObject = createProjectConfig(cloneDeep(datafile as any)); + optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); + projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); + projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig( + projectSimilarRuleKeyConfigObject, + JSON.stringify(similarRuleKeyDatafile) + ); + projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig( + projectSimilarExperimentKeyConfigObject, + JSON.stringify(similarExperimentKeyDatafile) + ); + }); + + it('should return all experiments except rollouts', () => { + const experimentsMap = optimizelyConfigObject.experimentsMap; + const experimentsCount = Object.keys(experimentsMap).length; + + expect(experimentsCount).toBe(12); + + const allExperiments: Experiment[] = getAllExperimentsFromDatafile(datafile); + + allExperiments.forEach(experiment => { + expect(experimentsMap[experiment.key]).toMatchObject({ + id: experiment.id, + key: experiment.key, + }); + + const variationsMap = experimentsMap[experiment.key].variationsMap; + + experiment.variations.forEach(variation => { + expect(variationsMap[variation.key]).toMatchObject({ + id: variation.id, + key: variation.key, + }); + }); + }); + }); + + it('should keep the last experiment in case of duplicate key and log a warning', () => { + const datafile = getDuplicateExperimentKeyConfig(); + const configObj = createProjectConfig(datafile, JSON.stringify(datafile)); + const optimizelyConfig = createOptimizelyConfig(configObj, JSON.stringify(datafile), logger); + const experimentsMap = optimizelyConfig.experimentsMap; + const duplicateKey = 'experiment_rule'; + const lastExperiment = datafile.experiments[datafile.experiments.length - 1]; + + expect(experimentsMap['experiment_rule'].id).toBe(lastExperiment.id); + expect(logger.warn).toHaveBeenCalledWith(`Duplicate experiment keys found in datafile: ${duplicateKey}`); + }); + + it('should return all the feature flags', function() { + const featureFlagsCount = Object.keys(optimizelyConfigObject.featuresMap).length; + assert.equal(featureFlagsCount, 9); + + const featuresMap = optimizelyConfigObject.featuresMap; + const expectedDeliveryRules = [ + [ + { + id: '594031', + key: '594031', + audiences: '', + variationsMap: { + '594032': { + id: '594032', + key: '594032', + featureEnabled: true, + variablesMap: { + new_content: { + id: '4919852825313280', + key: 'new_content', + type: 'boolean', + value: 'true', + }, + lasers: { + id: '5482802778734592', + key: 'lasers', + type: 'integer', + value: '395', + }, + price: { + id: '6045752732155904', + key: 'price', + type: 'double', + value: '4.99', + }, + message: { + id: '6327227708866560', + key: 'message', + type: 'string', + value: 'Hello audience', + }, + message_info: { + id: '8765345281230956', + key: 'message_info', + type: 'json', + value: '{ "count": 2, "message": "Hello audience" }', + }, + }, + }, + }, + }, + { + id: '594037', + key: '594037', + audiences: '', + variationsMap: { + '594038': { + id: '594038', + key: '594038', + featureEnabled: false, + variablesMap: { + new_content: { + id: '4919852825313280', + key: 'new_content', + type: 'boolean', + value: 'false', + }, + lasers: { + id: '5482802778734592', + key: 'lasers', + type: 'integer', + value: '400', + }, + price: { + id: '6045752732155904', + key: 'price', + type: 'double', + value: '14.99', + }, + message: { + id: '6327227708866560', + key: 'message', + type: 'string', + value: 'Hello', + }, + message_info: { + id: '8765345281230956', + key: 'message_info', + type: 'json', + value: '{ "count": 1, "message": "Hello" }', + }, + }, + }, + }, + }, + ], + [ + { + id: '594060', + key: '594060', + audiences: '', + variationsMap: { + '594061': { + id: '594061', + key: '594061', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '27.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is NOT coming', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '10003', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'false', + }, + }, + }, + }, + }, + { + id: '594066', + key: '594066', + audiences: '', + variationsMap: { + '594067': { + id: '594067', + key: '594067', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '30.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is coming definitely', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '500', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'true', + }, + }, + }, + }, + }, + ], + [], + [], + [ + { + id: '599056', + key: '599056', + audiences: '', + variationsMap: { + '599057': { + id: '599057', + key: '599057', + featureEnabled: true, + variablesMap: { + lasers: { + id: '4937719889264640', + key: 'lasers', + type: 'integer', + value: '200', + }, + message: { + id: '6345094772817920', + key: 'message', + type: 'string', + value: "i'm a rollout", + }, + }, + }, + }, + }, + ], + [], + [], + [ + { + id: '594060', + key: '594060', + audiences: '', + variationsMap: { + '594061': { + id: '594061', + key: '594061', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '27.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is NOT coming', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '10003', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'false', + }, + }, + }, + }, + }, + { + id: '594066', + key: '594066', + audiences: '', + variationsMap: { + '594067': { + id: '594067', + key: '594067', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '30.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is coming definitely', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '500', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'true', + }, + }, + }, + }, + }, + ], + [ + { + id: '594060', + key: '594060', + audiences: '', + variationsMap: { + '594061': { + id: '594061', + key: '594061', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '27.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is NOT coming', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '10003', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'false', + }, + }, + }, + }, + }, + { + id: '594066', + key: '594066', + audiences: '', + variationsMap: { + '594067': { + id: '594067', + key: '594067', + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: '5060590313668608', + key: 'miles_to_the_wall', + type: 'double', + value: '30.34', + }, + motto: { + id: '5342065290379264', + key: 'motto', + type: 'string', + value: 'Winter is coming definitely', + }, + soldiers_available: { + id: '6186490220511232', + key: 'soldiers_available', + type: 'integer', + value: '500', + }, + is_winter_coming: { + id: '6467965197221888', + key: 'is_winter_coming', + type: 'boolean', + value: 'true', + }, + }, + }, + }, + }, + ], + ]; + const expectedExperimentRules = [ + [], + [], + [ + { + id: '594098', + key: 'testing_my_feature', + audiences: '', + variationsMap: { + variation: { + id: '594096', + key: 'variation', + featureEnabled: true, + variablesMap: { + num_buttons: { + id: '4792309476491264', + key: 'num_buttons', + type: 'integer', + value: '2', + }, + is_button_animated: { + id: '5073784453201920', + key: 'is_button_animated', + type: 'boolean', + value: 'true', + }, + button_txt: { + id: '5636734406623232', + key: 'button_txt', + type: 'string', + value: 'Buy me NOW', + }, + button_width: { + id: '6199684360044544', + key: 'button_width', + type: 'double', + value: '20.25', + }, + button_info: { + id: '1547854156498475', + key: 'button_info', + type: 'json', + value: '{ "num_buttons": 1, "text": "first variation"}', + }, + }, + }, + control: { + id: '594097', + key: 'control', + featureEnabled: true, + variablesMap: { + num_buttons: { + id: '4792309476491264', + key: 'num_buttons', + type: 'integer', + value: '10', + }, + is_button_animated: { + id: '5073784453201920', + key: 'is_button_animated', + type: 'boolean', + value: 'false', + }, + button_txt: { + id: '5636734406623232', + key: 'button_txt', + type: 'string', + value: 'Buy me', + }, + button_width: { + id: '6199684360044544', + key: 'button_width', + type: 'double', + value: '50.55', + }, + button_info: { + id: '1547854156498475', + key: 'button_info', + type: 'json', + value: '{ "num_buttons": 2, "text": "second variation"}', + }, + }, + }, + variation2: { + id: '594099', + key: 'variation2', + featureEnabled: false, + variablesMap: { + num_buttons: { + id: '4792309476491264', + key: 'num_buttons', + type: 'integer', + value: '10', + }, + is_button_animated: { + id: '5073784453201920', + key: 'is_button_animated', + type: 'boolean', + value: 'false', + }, + button_txt: { + id: '5636734406623232', + key: 'button_txt', + type: 'string', + value: 'Buy me', + }, + button_width: { + id: '6199684360044544', + key: 'button_width', + type: 'double', + value: '50.55', + }, + button_info: { + id: '1547854156498475', + key: 'button_info', + type: 'json', + value: '{ "num_buttons": 0, "text": "default value"}', + }, + }, + }, + }, + }, + ], + [ + { + id: '595010', + key: 'exp_with_group', + audiences: '', + variationsMap: { + var: { + featureEnabled: undefined, + id: '595008', + key: 'var', + variablesMap: {}, + }, + con: { + featureEnabled: undefined, + id: '595009', + key: 'con', + variablesMap: {}, + }, + }, + }, + ], + [ + { + id: '599028', + key: 'test_shared_feature', + audiences: '', + variationsMap: { + treatment: { + id: '599026', + key: 'treatment', + featureEnabled: true, + variablesMap: { + lasers: { + id: '4937719889264640', + key: 'lasers', + type: 'integer', + value: '100', + }, + message: { + id: '6345094772817920', + key: 'message', + type: 'string', + value: 'shared', + }, + }, + }, + control: { + id: '599027', + key: 'control', + featureEnabled: false, + variablesMap: { + lasers: { + id: '4937719889264640', + key: 'lasers', + type: 'integer', + value: '100', + }, + message: { + id: '6345094772817920', + key: 'message', + type: 'string', + value: 'shared', + }, + }, + }, + }, + }, + ], + [], + [ + { + id: '12115595439', + key: 'no_traffic_experiment', + audiences: '', + variationsMap: { + variation_5000: { + featureEnabled: undefined, + id: '12098126629', + key: 'variation_5000', + variablesMap: {}, + }, + variation_10000: { + featureEnabled: undefined, + id: '12098126630', + key: 'variation_10000', + variablesMap: {}, + }, + }, + }, + ], + [ + { + id: '42222', + key: 'group_2_exp_1', + audiences: '"Test attribute users 3"', + variationsMap: { + var_1: { + id: '38901', + key: 'var_1', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '42223', + key: 'group_2_exp_2', + audiences: '"Test attribute users 3"', + variationsMap: { + var_1: { + id: '38905', + key: 'var_1', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '42224', + key: 'group_2_exp_3', + audiences: '"Test attribute users 3"', + variationsMap: { + var_1: { + id: '38906', + key: 'var_1', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + ], + [ + { + id: '111134', + key: 'test_experiment3', + audiences: '"Test attribute users 3"', + variationsMap: { + control: { + id: '222239', + key: 'control', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '111135', + key: 'test_experiment4', + audiences: '"Test attribute users 3"', + variationsMap: { + control: { + id: '222240', + key: 'control', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + { + id: '111136', + key: 'test_experiment5', + audiences: '"Test attribute users 3"', + variationsMap: { + control: { + id: '222241', + key: 'control', + featureEnabled: false, + variablesMap: {}, + }, + }, + }, + ], + ]; + + datafile.featureFlags.forEach((featureFlag, index) => { + expect(featuresMap[featureFlag.key]).toMatchObject({ + id: featureFlag.id, + key: featureFlag.key, + }); + + featureFlag.experimentIds.forEach(experimentId => { + const experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + + expect(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]).toBe(true); + }); + + const variablesMap = featuresMap[featureFlag.key].variablesMap; + const deliveryRules = featuresMap[featureFlag.key].deliveryRules; + const experimentRules = featuresMap[featureFlag.key].experimentRules; + + expect(deliveryRules).toEqual(expectedDeliveryRules[index]); + expect(experimentRules).toEqual(expectedExperimentRules[index]); + + featureFlag.variables.forEach(variable => { + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + const expectedVariableType = variable.type === 'string' && variable.subType === 'json' ? 'json' : variable.type; + + expect(variablesMap[variable.key]).toMatchObject({ + id: variable.id, + key: variable.key, + type: expectedVariableType, + value: variable.defaultValue, + }); + }); + }); + }); + + it('should correctly merge all feature variables', () => { + const featureFlags = datafile.featureFlags; + const datafileExperimentsMap: Record<string, Experiment> = getAllExperimentsFromDatafile(datafile).reduce( + (experiments, experiment) => { + experiments[experiment.key] = experiment; + return experiments; + }, + {} as Record<string, Experiment> + ); + + featureFlags.forEach(featureFlag => { + const experimentIds = featureFlag.experimentIds; + experimentIds.forEach(experimentId => { + const experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + const experiment = optimizelyConfigObject.experimentsMap[experimentKey]; + const variations = datafileExperimentsMap[experimentKey].variations; + const variationsMap = experiment.variationsMap; + variations.forEach(variation => { + featureFlag.variables.forEach(variable => { + const variableToAssert = variationsMap[variation.key].variablesMap[variable.key]; + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + const expectedVariableType = + variable.type === 'string' && variable.subType === 'json' ? 'json' : variable.type; + + expect({ + id: variable.id, + key: variable.key, + type: expectedVariableType, + }).toMatchObject({ + id: variableToAssert.id, + key: variableToAssert.key, + type: variableToAssert.type, + }); + + if (!variation.featureEnabled) { + expect(variable.defaultValue).toBe(variableToAssert.value); + } + }); + }); + }); + }); + }); + + it('should return correct config revision', () => { + expect(optimizelyConfigObject.revision).toBe(datafile.revision); + }); + + it('should return correct config sdkKey ', () => { + expect(optimizelyConfigObject.sdkKey).toBe(datafile.sdkKey); + }); + + it('should return correct config environmentKey ', () => { + expect(optimizelyConfigObject.environmentKey).toBe(datafile.environmentKey); + }); + + it('should return serialized audiences', () => { + const audiencesById = projectTypedAudienceConfigObject.audiencesById; + const audienceConditions = [ + ['or', '3468206642', '3988293898'], + ['or', '3468206642', '3988293898', '3468206646'], + ['not', '3468206642'], + ['or', '3468206642'], + ['and', '3468206642'], + ['3468206642'], + ['3468206642', '3988293898'], + ['and', ['or', '3468206642', '3988293898'], '3468206646'], + [ + 'and', + ['or', '3468206642', ['and', '3988293898', '3468206646']], + ['and', '3988293899', ['or', '3468206647', '3468206643']], + ], + ['and', 'and'], + ['not', ['and', '3468206642', '3988293898']], + [], + ['or', '3468206642', '999999999'], + ]; + + const expectedAudienceOutputs = [ + '"exactString" OR "substringString"', + '"exactString" OR "substringString" OR "exactNumber"', + 'NOT "exactString"', + '"exactString"', + '"exactString"', + '"exactString"', + '"exactString" OR "substringString"', + '("exactString" OR "substringString") AND "exactNumber"', + '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', + '', + 'NOT ("exactString" AND "substringString")', + '', + '"exactString" OR "999999999"', + ]; + + for (let testNo = 0; testNo < audienceConditions.length; testNo++) { + const serializedAudiences = OptimizelyConfig.getSerializedAudiences( + audienceConditions[testNo] as string[], + audiencesById + ); + + expect(serializedAudiences).toBe(expectedAudienceOutputs[testNo]); + } + }); + + it('should return correct rollouts', () => { + const rolloutFlag1 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_1'].deliveryRules[0]; + const rolloutFlag2 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_2'].deliveryRules[0]; + const rolloutFlag3 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_3'].deliveryRules[0]; + + expect(rolloutFlag1.id).toBe('9300000004977'); + expect(rolloutFlag1.key).toBe('targeted_delivery'); + expect(rolloutFlag2.id).toBe('9300000004979'); + expect(rolloutFlag2.key).toBe('targeted_delivery'); + expect(rolloutFlag3.id).toBe('9300000004981'); + expect(rolloutFlag3.key).toBe('targeted_delivery'); + }); + + it('should return default SDK and environment key', () => { + expect(optimizelySimilarRuleKeyConfigObject.sdkKey).toBe(''); + expect(optimizelySimilarRuleKeyConfigObject.environmentKey).toBe(''); + }); + + it('should return correct experiments with similar keys', function() { + expect(Object.keys(optimizelySimilarExperimentkeyConfigObject.experimentsMap).length).toBe(1); + + const experimentMapFlag1 = optimizelySimilarExperimentkeyConfigObject.featuresMap['flag1'].experimentsMap; + const experimentMapFlag2 = optimizelySimilarExperimentkeyConfigObject.featuresMap['flag2'].experimentsMap; + + expect(experimentMapFlag1['targeted_delivery'].id).toBe('9300000007569'); + expect(experimentMapFlag2['targeted_delivery'].id).toBe('9300000007573'); + }); +}); diff --git a/lib/project_config/optimizely_config.tests.js b/lib/project_config/optimizely_config.tests.js new file mode 100644 index 000000000..22d2b95f3 --- /dev/null +++ b/lib/project_config/optimizely_config.tests.js @@ -0,0 +1,916 @@ +/** + * Copyright 2019-2021, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; +import { cloneDeep } from 'lodash'; +import sinon from 'sinon'; + +import { createOptimizelyConfig, OptimizelyConfig } from './optimizely_config'; +import { createProjectConfig } from './project_config'; +import { + getTestProjectConfigWithFeatures, + getTypedAudiencesConfig, + getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig, + getDuplicateExperimentKeyConfig, +} from '../tests/test_data'; + +var datafile = getTestProjectConfigWithFeatures(); +var typedAudienceDatafile = getTypedAudiencesConfig(); +var similarRuleKeyDatafile = getSimilarRuleKeyConfig(); +var similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); + +var getAllExperimentsFromDatafile = function(datafile) { + var allExperiments = []; + datafile.groups.forEach(function(group) { + group.experiments.forEach(function(experiment) { + allExperiments.push(experiment); + }); + }); + datafile.experiments.forEach(function(experiment) { + allExperiments.push(experiment); + }); + return allExperiments; +}; + +describe('lib/core/optimizely_config', function() { + describe('Optimizely Config', function() { + var optimizelyConfigObject; + var projectConfigObject; + var optimizelyTypedAudienceConfigObject; + var projectTypedAudienceConfigObject; + var optimizelySimilarRuleKeyConfigObject; + var projectSimilarRuleKeyConfigObject; + var optimizelySimilarExperimentkeyConfigObject; + var projectSimilarExperimentKeyConfigObject; + + beforeEach(function() { + projectConfigObject = createProjectConfig(cloneDeep(datafile)); + optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); + projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); + optimizelyTypedAudienceConfigObject = createOptimizelyConfig(projectTypedAudienceConfigObject, JSON.stringify(typedAudienceDatafile)); + projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig(projectSimilarRuleKeyConfigObject, JSON.stringify(similarRuleKeyDatafile)); + projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig(projectSimilarExperimentKeyConfigObject, JSON.stringify(similarExperimentKeyDatafile)); + }); + + it('should return all experiments except rollouts', function() { + var experimentsMap = optimizelyConfigObject.experimentsMap; + var experimentsCount = Object.keys(optimizelyConfigObject.experimentsMap).length; + assert.equal(experimentsCount, 12); + + var allExperiments = getAllExperimentsFromDatafile(datafile); + allExperiments.forEach(function(experiment) { + assert.include(experimentsMap[experiment.key], { + id: experiment.id, + key: experiment.key, + }); + var variationsMap = experimentsMap[experiment.key].variationsMap; + experiment.variations.forEach(function(variation) { + assert.include(variationsMap[variation.key], { + id: variation.id, + key: variation.key, + }); + }); + }); + }); + + it('should keep the last experiment in case of duplicate key and log a warning', function() { + const datafile = getDuplicateExperimentKeyConfig(); + const configObj = createProjectConfig(datafile, JSON.stringify(datafile)); + + const logger = { + warn: sinon.spy(), + } + + const optimizelyConfig = createOptimizelyConfig(configObj, JSON.stringify(datafile), logger); + const experimentsMap = optimizelyConfig.experimentsMap; + + const duplicateKey = 'experiment_rule'; + const lastExperiment = datafile.experiments[datafile.experiments.length - 1]; + + assert.equal(experimentsMap['experiment_rule'].id, lastExperiment.id); + assert.isTrue(logger.warn.calledWithExactly(`Duplicate experiment keys found in datafile: ${duplicateKey}`)); + }); + + it('should return all the feature flags', function() { + var featureFlagsCount = Object.keys(optimizelyConfigObject.featuresMap).length; + assert.equal(featureFlagsCount, 9); + + var featuresMap = optimizelyConfigObject.featuresMap; + var expectedDeliveryRules = [ + [ + { + id: "594031", + key: "594031", + audiences: "", + variationsMap: { + "594032": { + id: "594032", + key: "594032", + featureEnabled: true, + variablesMap: { + new_content: { + id: "4919852825313280", + key: "new_content", + type: "boolean", + value: "true" + }, + lasers: { + id: "5482802778734592", + key: "lasers", + type: "integer", + value: "395" + }, + price: { + id: "6045752732155904", + key: "price", + type: "double", + value: "4.99" + }, + message: { + id: "6327227708866560", + key: "message", + type: "string", + value: "Hello audience" + }, + message_info: { + id: "8765345281230956", + key: "message_info", + type: "json", + value: "{ \"count\": 2, \"message\": \"Hello audience\" }" + } + } + } + } + }, { + id: "594037", + key: "594037", + audiences: "", + variationsMap: { + "594038": { + id: "594038", + key: "594038", + featureEnabled: false, + variablesMap: { + new_content: { + id: "4919852825313280", + key: "new_content", + type: "boolean", + value: "false" + }, + lasers: { + id: "5482802778734592", + key: "lasers", + type: "integer", + value: "400" + }, + price: { + id: "6045752732155904", + key: "price", + type: "double", + value: "14.99" + }, + message: { + id: "6327227708866560", + key: "message", + type: "string", + value: "Hello" + }, + message_info: { + id: "8765345281230956", + key: "message_info", + type: "json", + value: "{ \"count\": 1, \"message\": \"Hello\" }" + } + } + } + } + } + ], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ], + [], + [], + [ + { + id: "599056", + key: "599056", + audiences: "", + variationsMap: { + "599057": { + id: "599057", + key: "599057", + featureEnabled: true, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "200" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "i'm a rollout" + } + } + } + } + } + ], + [], + [], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ] + ] + var expectedExperimentRules = [ + [], + [], + [ + { + id: "594098", + key: "testing_my_feature", + audiences: "", + variationsMap: { + variation: { + id: "594096", + key: "variation", + featureEnabled: true, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "2" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "true" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me NOW" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "20.25" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 1, \"text\": \"first variation\"}" + } + } + }, + control: { + id: "594097", + key: "control", + featureEnabled: true, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "10" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "false" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "50.55" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 2, \"text\": \"second variation\"}" + } + } + }, + "variation2": { + id: "594099", + key: "variation2", + featureEnabled: false, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "10" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "false" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "50.55" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 0, \"text\": \"default value\"}" + } + } + } + } + } + ], + [ + { + id: "595010", + key: "exp_with_group", + audiences: "", + variationsMap: { + var: { + featureEnabled: undefined, + id: "595008", + key: "var", + variablesMap: {} + }, + con: { + featureEnabled: undefined, + id: "595009", + key: "con", + variablesMap: {} + } + } + } + ], + [ + { + id: "599028", + key: "test_shared_feature", + audiences: "", + variationsMap: { + treatment: { + id: "599026", + key: "treatment", + featureEnabled: true, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "100" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "shared" + } + } + }, + control: { + id: "599027", + key: "control", + featureEnabled: false, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "100" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "shared" + } + } + } + } + } + ], + [], + [ + { + id: "12115595439", + key: "no_traffic_experiment", + audiences: "", + variationsMap: { + "variation_5000": { + "featureEnabled": undefined, + id: "12098126629", + key: "variation_5000", + variablesMap: {} + }, + "variation_10000": { + "featureEnabled": undefined, + id: "12098126630", + key: "variation_10000", + variablesMap: {} + } + } + } + ], + [ + { + id: "42222", + key: "group_2_exp_1", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38901", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "42223", + key: "group_2_exp_2", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38905", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "42224", + key: "group_2_exp_3", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38906", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + } + ], + [ + { + id: "111134", + key: "test_experiment3", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222239", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "111135", + key: "test_experiment4", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222240", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "111136", + key: "test_experiment5", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222241", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + } + ] + ] + + datafile.featureFlags.forEach(function(featureFlag, index) { + assert.include(featuresMap[featureFlag.key], { + id: featureFlag.id, + key: featureFlag.key, + }); + featureFlag.experimentIds.forEach(function(experimentId) { + var experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + assert.isTrue(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]); + }); + var variablesMap = featuresMap[featureFlag.key].variablesMap; + var deliveryRules = featuresMap[featureFlag.key].deliveryRules; + var experimentRules = featuresMap[featureFlag.key].experimentRules; + assert.deepEqual(deliveryRules, expectedDeliveryRules[index]); + assert.deepEqual(experimentRules, expectedExperimentRules[index]); + featureFlag.variables.forEach(function(variable) { + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + var expectedVariableType = (variable.type === "string" && variable.subType === "json") ? "json" : variable.type; + assert.include(variablesMap[variable.key], { + id: variable.id, + key: variable.key, + type: expectedVariableType, + value: variable.defaultValue, + }); + }); + }); + }); + + it('should correctly merge all feature variables', function() { + var featureFlags = datafile.featureFlags; + var datafileExperimentsMap = getAllExperimentsFromDatafile(datafile).reduce(function(experiments, experiment) { + experiments[experiment.key] = experiment; + return experiments; + }, {}); + featureFlags.forEach(function(featureFlag) { + var experimentIds = featureFlag.experimentIds; + experimentIds.forEach(function(experimentId) { + var experimentKey = projectConfigObject.experimentIdMap[experimentId].key; + var experiment = optimizelyConfigObject.experimentsMap[experimentKey]; + var variations = datafileExperimentsMap[experimentKey].variations; + var variationsMap = experiment.variationsMap; + variations.forEach(function(variation) { + featureFlag.variables.forEach(function(variable) { + var variableToAssert = variationsMap[variation.key].variablesMap[variable.key]; + // json is represented as sub type of string to support backwards compatibility in datafile. + // project config treats it as a first-class type. + var expectedVariableType = (variable.type === "string" && variable.subType === "json") ? "json" : variable.type; + assert.include( + { + id: variable.id, + key: variable.key, + type: expectedVariableType, + }, + { + id: variableToAssert.id, + key: variableToAssert.key, + type: variableToAssert.type, + } + ); + if (!variation.featureEnabled) { + assert.equal(variable.defaultValue, variableToAssert.value); + } + }); + }); + }); + }); + }); + + it('should return correct config revision', function() { + assert.equal(optimizelyConfigObject.revision, datafile.revision); + }); + + it('should return correct config sdkKey ', function() { + assert.equal(optimizelyConfigObject.sdkKey, datafile.sdkKey); + }); + + it('should return correct config environmentKey ', function() { + assert.equal(optimizelyConfigObject.environmentKey, datafile.environmentKey); + }); + + it('should return serialized audiences', function () { + const audiencesById = projectTypedAudienceConfigObject.audiencesById; + const audienceConditions = [ + ['or', '3468206642', '3988293898'], + ['or', '3468206642', '3988293898', '3468206646'], + ['not', '3468206642'], + ['or', '3468206642'], + ['and', '3468206642'], + ['3468206642'], + ['3468206642', '3988293898'], + ['and', ['or', '3468206642', '3988293898'], '3468206646'], + [ + 'and', + ['or', '3468206642', ['and', '3988293898', '3468206646']], + ['and', '3988293899', ['or', '3468206647', '3468206643']], + ], + ['and', 'and'], + ['not', ['and', '3468206642', '3988293898']], + [], + ['or', '3468206642', '999999999'], + ]; + + const expectedAudienceOutputs = [ + '"exactString" OR "substringString"', + '"exactString" OR "substringString" OR "exactNumber"', + 'NOT "exactString"', + '"exactString"', + '"exactString"', + '"exactString"', + '"exactString" OR "substringString"', + '("exactString" OR "substringString") AND "exactNumber"', + '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', + '', + 'NOT ("exactString" AND "substringString")', + '', + '"exactString" OR "999999999"', + ]; + + for (let testNo = 0; testNo < audienceConditions.length; testNo++) { + const serializedAudiences = OptimizelyConfig.getSerializedAudiences(audienceConditions[testNo], audiencesById); + assert.equal(serializedAudiences, expectedAudienceOutputs[testNo]); + } + }); + + it('should return correct rollouts', function () { + const rolloutFlag1 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_1'].deliveryRules[0]; + const rolloutFlag2 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_2'].deliveryRules[0]; + const rolloutFlag3 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_3'].deliveryRules[0]; + + assert.equal(rolloutFlag1.id, '9300000004977'); + assert.equal(rolloutFlag1.key, 'targeted_delivery'); + assert.equal(rolloutFlag2.id, '9300000004979'); + assert.equal(rolloutFlag2.key, 'targeted_delivery'); + assert.equal(rolloutFlag3.id, '9300000004981'); + assert.equal(rolloutFlag3.key, 'targeted_delivery'); + + }); + + it('should return default SDK and environment key', function() { + + assert.equal(optimizelySimilarRuleKeyConfigObject.sdkKey, ""); + assert.equal(optimizelySimilarRuleKeyConfigObject.environmentKey, ""); + + }); + + it('should return correct experiments with similar keys', function() { + + assert.equal(Object.keys(optimizelySimilarExperimentkeyConfigObject.experimentsMap).length, 1); + const experimentMapFlag1 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag1"].experimentsMap; + const experimentMapFlag2 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag2"].experimentsMap; + assert.equal(experimentMapFlag1["targeted_delivery"].id, "9300000007569"); + assert.equal(experimentMapFlag2["targeted_delivery"].id, "9300000007573"); + + }); + }); +}); diff --git a/lib/project_config/optimizely_config.ts b/lib/project_config/optimizely_config.ts new file mode 100644 index 000000000..b01255c43 --- /dev/null +++ b/lib/project_config/optimizely_config.ts @@ -0,0 +1,483 @@ +/** + * Copyright 2020-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from '../logging/logger' +import { ProjectConfig } from '../project_config/project_config'; +import { DEFAULT_OPERATOR_TYPES } from '../core/condition_tree_evaluator'; +import { + Audience, + Experiment, + FeatureVariable, + OptimizelyAttribute, + OptimizelyAudience, + OptimizelyEvent, + OptimizelyExperiment, + OptimizelyExperimentsMap, + OptimizelyFeaturesMap, + OptimizelyVariable, + OptimizelyVariablesMap, + OptimizelyVariation, + Rollout, + Variation, + VariationVariable, +} from '../shared_types'; + +interface FeatureVariablesMap { + [key: string]: FeatureVariable[]; +} + +/** + * The OptimizelyConfig class + * @param {ProjectConfig} configObj + * @param {string} datafile + */ +export class OptimizelyConfig { + public environmentKey: string; + public sdkKey: string; + public revision: string; + + /** + * This experimentsMap is for experiments of legacy projects only. + * For flag projects, experiment keys are not guaranteed to be unique + * across multiple flags, so this map may not include all experiments + * when keys conflict. + */ + public experimentsMap: OptimizelyExperimentsMap; + + public featuresMap: OptimizelyFeaturesMap; + public attributes: OptimizelyAttribute[]; + public audiences: OptimizelyAudience[]; + public events: OptimizelyEvent[]; + private datafile: string; + + + constructor(configObj: ProjectConfig, datafile: string, logger?: LoggerFacade) { + this.sdkKey = configObj.sdkKey ?? ''; + this.environmentKey = configObj.environmentKey ?? ''; + this.attributes = configObj.attributes; + this.audiences = OptimizelyConfig.getAudiences(configObj); + this.events = configObj.events; + this.revision = configObj.revision; + + const featureIdVariablesMap = (configObj.featureFlags || []).reduce((resultMap: FeatureVariablesMap, feature) => { + resultMap[feature.id] = feature.variables; + return resultMap; + }, {}); + + const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); + + const { experimentsMapById, experimentsMapByKey } = OptimizelyConfig.getExperimentsMap( + configObj, featureIdVariablesMap, variableIdMap, logger, + ); + + this.experimentsMap = experimentsMapByKey; + + this.featuresMap = OptimizelyConfig.getFeaturesMap( + configObj, featureIdVariablesMap, experimentsMapById, variableIdMap + ); + this.datafile = datafile; + } + + /** + * Get the datafile + * @returns {string} JSON string representation of the datafile that was used to create the current config object + */ + getDatafile(): string { + return this.datafile; + } + + /** + * Get Unique audiences list with typedAudiences as priority + * @param {ProjectConfig} configObj + * @returns {OptimizelyAudience[]} Array of unique audiences + */ + static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { + const audiences: OptimizelyAudience[] = []; + const typedAudienceIds: string[] = []; + + (configObj.typedAudiences || []).forEach((typedAudience) => { + audiences.push({ + id: typedAudience.id, + conditions: JSON.stringify(typedAudience.conditions), + name: typedAudience.name, + }); + typedAudienceIds.push(typedAudience.id); + }); + + (configObj.audiences || []).forEach((audience) => { + if (typedAudienceIds.indexOf(audience.id) === -1 && audience.id != '$opt_dummy_audience') { + audiences.push({ + id: audience.id, + conditions: JSON.stringify(audience.conditions), + name: audience.name, + }); + } + }); + + return audiences; + } + + /** + * Converts list of audience conditions to serialized audiences used in experiment + * for examples: + * 1. Input: ["or", "1", "2"] + * Output: "\"us\" OR \"female\"" + * 2. Input: ["not", "1"] + * Output: "NOT \"us\"" + * 3. Input: ["or", "1"] + * Output: "\"us\"" + * 4. Input: ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "13"]]] + * Output: "(\"us\" OR (\"female\" AND \"adult\")) AND (\"fr\" AND (\"male\" OR \"kid\"))" + * @param {Array<string | string[]>} conditions + * @param {[id: string]: Audience} audiencesById + * @returns {string} Serialized audiences condition string + */ + static getSerializedAudiences( + conditions: Array<string | string[]>, + audiencesById: { [id: string]: Audience } + ): string { + let serializedAudience = ''; + + if (conditions) { + let cond = ''; + conditions.forEach((item) => { + let subAudience = ''; + // Checks if item is list of conditions means it is sub audience + if (item instanceof Array) { + subAudience = OptimizelyConfig.getSerializedAudiences(item, audiencesById); + subAudience = `(${subAudience})`; + } else if (DEFAULT_OPERATOR_TYPES.indexOf(item) > -1) { + cond = item.toUpperCase(); + } else { + // Checks if item is audience id + const audienceName = audiencesById[item] ? audiencesById[item].name : item; + // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in serializedAudience then append condition between serializedAudience and item + if (serializedAudience || cond === 'NOT') { + cond = cond === '' ? 'OR' : cond; + if (serializedAudience === '') { + serializedAudience = `${cond} "${audiencesById[item].name}"`; + } else { + serializedAudience = serializedAudience.concat(` ${cond} "${audienceName}"`); + } + } else { + serializedAudience = `"${audienceName}"`; + } + } + // Checks if sub audience is empty or not + if (subAudience !== '') { + if (serializedAudience !== '' || cond === 'NOT') { + cond = cond === '' ? 'OR' : cond; + if (serializedAudience === '') { + serializedAudience = `${cond} ${subAudience}`; + } else { + serializedAudience = serializedAudience.concat(` ${cond} ${subAudience}`); + } + } else { + serializedAudience = serializedAudience.concat(subAudience); + } + } + }); + } + return serializedAudience; + } + + /** + * Get serialized audience condition string for experiment + * @param {Experiment} experiment + * @param {ProjectConfig} configObj + * @returns {string} Serialized audiences condition string + */ + static getExperimentAudiences(experiment: Experiment, configObj: ProjectConfig): string { + if (!experiment.audienceConditions) { + return ''; + } + return OptimizelyConfig.getSerializedAudiences(experiment.audienceConditions, configObj.audiencesById); + } + + /** + * Make map of featureVariable which are associated with given feature experiment + * @param {FeatureVariablesMap} featureIdVariableMap + * @param {[id: string]: FeatureVariable} variableIdMap + * @param {string} featureId + * @param {VariationVariable[] | undefined} featureVariableUsages + * @param {boolean | undefined} isFeatureEnabled + * @returns {OptimizelyVariablesMap} FeatureVariables mapped by key + */ + static mergeFeatureVariables( + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: { [id: string]: FeatureVariable }, + featureId: string, + featureVariableUsages: VariationVariable[] | undefined, + isFeatureEnabled: boolean | undefined + ): OptimizelyVariablesMap { + const variablesMap = (featureIdVariableMap[featureId] || []).reduce( + (optlyVariablesMap: OptimizelyVariablesMap, featureVariable) => { + optlyVariablesMap[featureVariable.key] = { + id: featureVariable.id, + key: featureVariable.key, + type: featureVariable.type, + value: featureVariable.defaultValue, + }; + return optlyVariablesMap; + }, + {} + ); + + (featureVariableUsages || []).forEach((featureVariableUsage) => { + const defaultVariable = variableIdMap[featureVariableUsage.id]; + const optimizelyVariable: OptimizelyVariable = { + id: featureVariableUsage.id, + key: defaultVariable.key, + type: defaultVariable.type, + value: isFeatureEnabled ? featureVariableUsage.value : defaultVariable.defaultValue, + }; + variablesMap[defaultVariable.key] = optimizelyVariable; + }); + return variablesMap; + } + + /** + * Gets Map of all experiment variations and variables including rollouts + * @param {Variation[]} variations + * @param {FeatureVariablesMap} featureIdVariableMap + * @param {{[id: string]: FeatureVariable}} variableIdMap + * @param {string} featureId + * @returns {[key: string]: Variation} Variations mapped by key + */ + static getVariationsMap( + variations: Variation[], + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: { [id: string]: FeatureVariable }, + featureId: string + ): { [key: string]: Variation } { + let variationsMap: { [key: string]: OptimizelyVariation } = {}; + variationsMap = variations.reduce((optlyVariationsMap: { [key: string]: OptimizelyVariation }, variation) => { + const variablesMap = OptimizelyConfig.mergeFeatureVariables( + featureIdVariableMap, + variableIdMap, + featureId, + variation.variables, + variation.featureEnabled + ); + optlyVariationsMap[variation.key] = { + id: variation.id, + key: variation.key, + featureEnabled: variation.featureEnabled, + variablesMap: variablesMap, + }; + return optlyVariationsMap; + }, {}); + + return variationsMap; + } + + /** + * Gets Map of FeatureVariable with respect to featureVariableId + * @param {ProjectConfig} configObj + * @returns {[id: string]: FeatureVariable} FeatureVariables mapped by id + */ + static getVariableIdMap(configObj: ProjectConfig): { [id: string]: FeatureVariable } { + let variablesIdMap: { [id: string]: FeatureVariable } = {}; + variablesIdMap = (configObj.featureFlags || []).reduce((resultMap: { [id: string]: FeatureVariable }, feature) => { + feature.variables.forEach((variable) => { + resultMap[variable.id] = variable; + }); + return resultMap; + }, {}); + + return variablesIdMap; + } + + /** + * Gets list of rollout experiments + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureVariableIdMap + * @param {string} featureId + * @param {Experiment[]} experiments + * @param {{[id: string]: FeatureVariable}} variableIdMap + * @returns {OptimizelyExperiment[]} List of Optimizely rollout experiments + */ + static getDeliveryRules( + configObj: ProjectConfig, + featureVariableIdMap: FeatureVariablesMap, + featureId: string, + experiments: Experiment[], + variableIdMap: {[id: string]: FeatureVariable} + ): OptimizelyExperiment[] { + return experiments.map((experiment) => { + return { + id: experiment.id, + key: experiment.key, + audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), + variationsMap: OptimizelyConfig.getVariationsMap( + experiment.variations, + featureVariableIdMap, + variableIdMap, + featureId + ), + }; + }); + } + + /** + * Get Experiment Ids which are part of rollout + * @param {Rollout[]} rollouts + * @returns {string[]} Array of experiment Ids + */ + static getRolloutExperimentIds(rollouts: Rollout[]): string[] { + const experimentIds: string[] = []; + (rollouts || []).forEach((rollout) => { + rollout.experiments.forEach((e) => { + experimentIds.push(e.id); + }); + }); + return experimentIds; + } + + /** + * Get experiments mapped by their id's which are not part of a rollout + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureIdVariableMap + * @param {{[id: string]: FeatureVariable}} variableIdMap + * @returns { experimentsMapById: { [id: string]: OptimizelyExperiment }, experimentsMapByKey: OptimizelyExperimentsMap } Experiments mapped by id and key + */ + static getExperimentsMap( + configObj: ProjectConfig, + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: {[id: string]: FeatureVariable}, + logger?: LoggerFacade, + ) : { experimentsMapById: { [id: string]: OptimizelyExperiment }, experimentsMapByKey: OptimizelyExperimentsMap } { + const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); + + const experimentsMapById: { [id : string]: OptimizelyExperiment } = {}; + const experimentsMapByKey: OptimizelyExperimentsMap = {}; + + const experiments = configObj.experiments || []; + experiments.forEach((experiment) => { + if (rolloutExperimentIds.indexOf(experiment.id) !== -1) { + return; + } + + const featureIds = configObj.experimentFeatureMap[experiment.id]; + let featureId = ''; + if (featureIds && featureIds.length > 0) { + featureId = featureIds[0]; + } + const variationsMap = OptimizelyConfig.getVariationsMap( + experiment.variations, + featureIdVariableMap, + variableIdMap, + featureId.toString() + ); + + const optimizelyExperiment: OptimizelyExperiment = { + id: experiment.id, + key: experiment.key, + audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), + variationsMap: variationsMap, + }; + + experimentsMapById[experiment.id] = optimizelyExperiment; + if (experimentsMapByKey[experiment.key] && logger) { + logger.warn(`Duplicate experiment keys found in datafile: ${experiment.key}`); + } + experimentsMapByKey[experiment.key] = optimizelyExperiment; + }); + + return { experimentsMapById, experimentsMapByKey }; + } + + /** + * Get experiments mapped by their keys + * @param {OptimizelyExperimentsMap} experimentsMapById + * @returns {OptimizelyExperimentsMap} Experiments mapped by key + */ + static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { + const experimentKeysMap: OptimizelyExperimentsMap = {}; + + for (const id in experimentsMapById) { + const experiment = experimentsMapById[id]; + experimentKeysMap[experiment.key] = experiment; + } + return experimentKeysMap; + } + + /** + * Gets Map of all FeatureFlags and associated experiment map inside it + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureVariableIdMap + * @param {OptimizelyExperimentsMap} experimentsMapById + * @param {{[id: string]: FeatureVariable}} variableIdMap + * @returns {OptimizelyFeaturesMap} OptimizelyFeature mapped by key + */ + static getFeaturesMap( + configObj: ProjectConfig, + featureVariableIdMap: FeatureVariablesMap, + experimentsMapById: OptimizelyExperimentsMap, + variableIdMap: {[id: string]: FeatureVariable} + ): OptimizelyFeaturesMap { + const featuresMap: OptimizelyFeaturesMap = {}; + configObj.featureFlags.forEach((featureFlag) => { + const featureExperimentMap: OptimizelyExperimentsMap = {}; + const experimentRules: OptimizelyExperiment[] = []; + featureFlag.experimentIds.forEach(experimentId => { + const experiment = experimentsMapById[experimentId]; + if (experiment) { + featureExperimentMap[experiment.key] = experiment; + } + experimentRules.push(experimentsMapById[experimentId]); + }); + const featureVariableMap = (featureFlag.variables || []).reduce((variables: OptimizelyVariablesMap, variable) => { + variables[variable.key] = { + id: variable.id, + key: variable.key, + type: variable.type, + value: variable.defaultValue, + }; + return variables; + }, {}); + let deliveryRules: OptimizelyExperiment[] = []; + const rollout = configObj.rolloutIdMap[featureFlag.rolloutId]; + if (rollout) { + deliveryRules = OptimizelyConfig.getDeliveryRules( + configObj, + featureVariableIdMap, + featureFlag.id, + rollout.experiments, + variableIdMap, + ); + } + featuresMap[featureFlag.key] = { + id: featureFlag.id, + key: featureFlag.key, + experimentRules: experimentRules, + deliveryRules: deliveryRules, + experimentsMap: featureExperimentMap, + variablesMap: featureVariableMap, + }; + }); + return featuresMap; + } +} + +/** + * Create an instance of OptimizelyConfig + * @param {ProjectConfig} configObj + * @param {string} datafile + * @returns {OptimizelyConfig} An instance of OptimizelyConfig + */ +export function createOptimizelyConfig(configObj: ProjectConfig, datafile: string, logger?: LoggerFacade): OptimizelyConfig { + return new OptimizelyConfig(configObj, datafile, logger); +} diff --git a/lib/project_config/polling_datafile_manager.spec.ts b/lib/project_config/polling_datafile_manager.spec.ts new file mode 100644 index 000000000..921ab2a93 --- /dev/null +++ b/lib/project_config/polling_datafile_manager.spec.ts @@ -0,0 +1,1013 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; + +import { LOGGER_NAME, PollingDatafileManager} from './polling_datafile_manager'; +import { getMockRepeater } from '../tests/mock/mock_repeater'; +import { getMockAbortableRequest, getMockRequestHandler } from '../tests/mock/mock_request_handler'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { DEFAULT_AUTHENTICATED_URL_TEMPLATE, DEFAULT_URL_TEMPLATE, MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './constant'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { ServiceState, StartupLog } from '../service'; +import { getMockSyncCache, getMockAsyncCache } from '../tests/mock/mock_cache'; +import { LogLevel } from '../logging/logger'; + + +describe('PollingDatafileManager', () => { + it('should set name on the logger passed into the constructor', () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const logger = getMockLogger(); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: '123', + logger, + }); + + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME) + }); + + it('should set name on the logger set by setLogger', () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const logger = getMockLogger(); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: '123', + }); + + manager.setLogger(logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME) + }); + + it('should log polling interval below MIN_UPDATE_INTERVAL', () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const logger = getMockLogger(); + + const startupLogs: StartupLog[] = [ + { + level: LogLevel.Warn, + message: 'warn message', + params: [1, 2] + }, + { + level: LogLevel.Error, + message: 'error message', + params: [3, 4] + }, + ]; + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: '123', + logger, + startupLogs, + }); + + manager.start(); + expect(logger.warn).toHaveBeenNthCalledWith(1, 'warn message', 1, 2); + expect(logger.error).toHaveBeenNthCalledWith(1, 'error message', 3, 4); + }); + + + it('starts the repeater with immediateExecution on start', () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: '123', + }); + manager.start(); + expect(repeater.start).toHaveBeenCalledWith(true); + }); + + describe('when cached datafile is available', () => { + it('uses cached version of datafile, resolves onRunning() and calls onUpdate handlers while datafile fetch request waits', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); // response promise is pending + const cache = getMockAsyncCache<string>(); + await cache.set('opt-datafile-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + cache, + }); + + manager.start(); + repeater.execute(0); + const listener = vi.fn(); + + manager.onUpdate(listener); + await expect(manager.onRunning()).resolves.toBeUndefined(); + expect(listener).toHaveBeenCalledWith(JSON.stringify({ name: 'keyThatExists' })); + }); + + it('uses cached version of datafile, resolves onRunning() and calls onUpdate handlers even if fetch request fails', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const cache = getMockAsyncCache<string>(); + await cache.set('opt-datafile-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + cache, + }); + + manager.start(); + repeater.execute(0); + + const listener = vi.fn(); + + manager.onUpdate(listener); + await expect(manager.onRunning()).resolves.toBeUndefined(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith(JSON.stringify({ name: 'keyThatExists' })); + }); + + it('uses cached version of datafile, then calls onUpdate when fetch request succeeds after the cache read', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const cache = getMockAsyncCache<string>(); + await cache.set('opt-datafile-keyThatExists', JSON.stringify({ name: 'keyThatExists' })); + const mockResponse = getMockAbortableRequest(); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + cache, + }); + + manager.start(); + repeater.execute(0); + + const listener = vi.fn(); + + manager.onUpdate(listener); + await expect(manager.onRunning()).resolves.toBeUndefined(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenNthCalledWith(1, JSON.stringify({ name: 'keyThatExists' })); + + mockResponse.mockResponse.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} }); + await mockResponse.mockResponse; + expect(listener).toHaveBeenNthCalledWith(2, '{"foo": "bar"}'); + }); + + it('ignores cached datafile if fetch request succeeds before cache read completes', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const cache = getMockAsyncCache<string>(); + // this will be resolved after the requestHandler response is resolved + const cachePromise = resolvablePromise<string | undefined>(); + const getSpy = vi.spyOn(cache, 'get'); + getSpy.mockReturnValueOnce(cachePromise.promise); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + cache, + }); + + manager.start(); + repeater.execute(0); + + const listener = vi.fn(); + + manager.onUpdate(listener); + await expect(manager.onRunning()).resolves.toBeUndefined(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith('{"foo": "bar"}'); + + cachePromise.resolve(JSON.stringify({ name: 'keyThatExists '})); + await cachePromise.promise; + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).not.toHaveBeenCalledWith(JSON.stringify({ name: 'keyThatExists' })); + }); + }); + + it('returns a failing promise to repeater if requestHandler.makeRequest return non-success status code', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 500, body: '', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + manager.start(); + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + }); + + it('returns a failing promise to repeater if requestHandler.makeRequest promise fails', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + manager.start(); + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + }); + + it('returns a promise that resolves to repeater if requestHandler.makeRequest succeedes', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + manager.start(); + const ret = repeater.execute(0); + await expect(ret).resolves.not.toThrow(); + }); + + describe('start', () => { + it('retries specified number of times before rejecting onRunning() and onTerminated() when autoupdate is true', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValue(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + initRetry: 5, + autoUpdate: true, + }); + + manager.start(); + + for(let i = 0; i < 6; i++) { + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + } + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(6); + await expect(manager.onRunning()).rejects.toThrow(); + await expect(manager.onTerminated()).rejects.toThrow(); + }); + + it('retries min(initRetry, 5) amount of times if datafile manager is disposable', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValue(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + initRetry: 10, + }); + manager.makeDisposable(); + manager.start(); + + for(let i = 0; i < 6; i++) { + const ret = repeater.execute(i); + await expect(ret).rejects.toThrow(); + } + + expect(repeater.isRunning()).toBe(false); + expect(() => repeater.execute(6)).toThrow(); + }) + + it('retries specified number of times before rejecting onRunning() and onTerminated() when autoupdate is false', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValue(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + initRetry: 5, + autoUpdate: false, + }); + + manager.start(); + + for(let i = 0; i < 6; i++) { + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + } + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(6); + await expect(manager.onRunning()).rejects.toThrow(); + await expect(manager.onTerminated()).rejects.toThrow(); + }); + + it('stops the repeater when initalization fails', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValue(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + initRetry: 0, + autoUpdate: false, + }); + + manager.start(); + repeater.execute(0); + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(1); + await expect(manager.onRunning()).rejects.toThrow(); + await expect(manager.onTerminated()).rejects.toThrow(); + expect(repeater.stop).toHaveBeenCalled(); + }); + + it('retries specified number of times before rejecting onRunning() and onTerminated() when provided cache does not contain datafile', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValue(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatDoesNotExists', + initRetry: 5, + cache: getMockAsyncCache(), + }); + + manager.start(); + + for(let i = 0; i < 6; i++) { + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + } + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(6); + await expect(manager.onRunning()).rejects.toThrow(); + await expect(manager.onTerminated()).rejects.toThrow(); + }); + + it('retries init indefinitely if initRetry is not provided when autoupdate is true', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + const promiseCallback = vi.fn(); + manager.onRunning().finally(promiseCallback); + + manager.start(); + const testTry = 10_000; + + for(let i = 0; i < testTry; i++) { + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + } + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(testTry); + expect(promiseCallback).not.toHaveBeenCalled(); + }); + + it('retries init indefinitely if initRetry is not provided when autoupdate is false', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: false, + }); + + const promiseCallback = vi.fn(); + manager.onRunning().finally(promiseCallback); + + manager.start(); + const testTry = 10_000; + + for(let i = 0; i < testTry; i++) { + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + } + + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(testTry); + expect(promiseCallback).not.toHaveBeenCalled(); + }); + + it('successfully resolves onRunning() and calls onUpdate handlers when fetch request succeeds', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + const listener = vi.fn(); + + manager.onUpdate(listener); + + manager.start(); + repeater.execute(0); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith('{"foo": "bar"}'); + }); + + it('successfully resolves onRunning() and calls onUpdate handlers when fetch request succeeds after retries', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockFailure = getMockAbortableRequest(Promise.reject('test error')); + const mockSuccess = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockFailure) + .mockReturnValueOnce(mockFailure).mockReturnValueOnce(mockSuccess); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + initRetry: 5, + }); + + const listener = vi.fn(); + + manager.onUpdate(listener); + + manager.start(); + for(let i = 0; i < 2; i++) { + const ret = repeater.execute(0); + await expect(ret).rejects.toThrow(); + } + + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(3); + expect(listener).toHaveBeenCalledWith('{"foo": "bar"}'); + }); + + it('stops repeater after successful initialization if autoupdate is false', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: false, + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(repeater.stop).toHaveBeenCalled(); + }); + + it('stops repeater after successful initialization if disposable is true', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + manager.makeDisposable(); + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(repeater.stop).toHaveBeenCalled(); + }); + + it('saves the datafile in cache', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const cache = getMockAsyncCache<string>(); + const spy = vi.spyOn(cache, 'set'); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatDoesNotExists', + cache, + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(spy).toHaveBeenCalledWith('opt-datafile-keyThatDoesNotExists', '{"foo": "bar"}'); + }); + }); + + describe('autoupdate', () => { + it('fetches datafile on each tick and calls onUpdate handlers when fetch request succeeds', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse1 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + const mockResponse2 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo2": "bar2"}', headers: {} })); + const mockResponse3 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo3": "bar3"}', headers: {} })); + + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2).mockReturnValueOnce(mockResponse3); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + + for(let i = 0; i <3; i++) { + const ret = repeater.execute(0); + await expect(ret).resolves.not.toThrow(); + } + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledTimes(3); + expect(listener).toHaveBeenNthCalledWith(1, '{"foo": "bar"}'); + expect(listener).toHaveBeenNthCalledWith(2, '{"foo2": "bar2"}'); + expect(listener).toHaveBeenNthCalledWith(3, '{"foo3": "bar3"}'); + }); + + it('saves the datafile each time in cache', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse1 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + const mockResponse2 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo2": "bar2"}', headers: {} })); + const mockResponse3 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo3": "bar3"}', headers: {} })); + + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2).mockReturnValueOnce(mockResponse3); + + const cache = getMockAsyncCache<string>(); + const spy = vi.spyOn(cache, 'set'); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatDoesNotExists', + autoUpdate: true, + cache, + }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + + for(let i = 0; i <3; i++) { + const ret = repeater.execute(0); + await expect(ret).resolves.not.toThrow(); + } + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(spy).toHaveBeenNthCalledWith(1, 'opt-datafile-keyThatDoesNotExists', '{"foo": "bar"}'); + expect(spy).toHaveBeenNthCalledWith(2, 'opt-datafile-keyThatDoesNotExists', '{"foo2": "bar2"}'); + expect(spy).toHaveBeenNthCalledWith(3, 'opt-datafile-keyThatDoesNotExists', '{"foo3": "bar3"}'); + }); + + it('logs an error if fetch request fails and does not call onUpdate handler', async () => { + const logger = getMockLogger(); + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse1 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + const mockResponse2= getMockAbortableRequest(Promise.reject('test error')); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2).mockReturnValueOnce(mockResponse2); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + logger, + }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + for(let i = 0; i < 3; i++) { + await repeater.execute(0).catch(() => {}); + } + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(logger.error).toHaveBeenCalledTimes(2); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('logs an error if fetch returns non success response and does not call onUpdate handler', async () => { + const logger = getMockLogger(); + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse1 = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + const mockResponse2= getMockAbortableRequest(Promise.resolve({ statusCode: 500, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2).mockReturnValueOnce(mockResponse2); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + logger, + }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + for(let i = 0; i < 3; i++) { + await repeater.execute(0).catch(() => {}); + } + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(logger.error).toHaveBeenCalledTimes(2); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('saves and uses last-modified header', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse1 = getMockAbortableRequest( + Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT' } })); + const mockResponse2 = getMockAbortableRequest( + Promise.resolve({ statusCode: 304, body: '', headers: {} })); + const mockResponse3 = getMockAbortableRequest( + Promise.resolve({ statusCode: 200, body: '{"foo2": "bar2"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2).mockReturnValueOnce(mockResponse3); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatDoesNotExists', + autoUpdate: true, + }); + + manager.start(); + + for(let i = 0; i <3; i++) { + await repeater.execute(0); + } + + await expect(manager.onRunning()).resolves.not.toThrow(); + const secondCallHeaders = requestHandler.makeRequest.mock.calls[1][1]; + expect(secondCallHeaders['if-modified-since']).toBe('Fri, 08 Mar 2019 18:57:17 GMT'); + const thirdCallHeaders = requestHandler.makeRequest.mock.calls[1][1]; + expect(thirdCallHeaders['if-modified-since']).toBe('Fri, 08 Mar 2019 18:57:17 GMT'); + }); + + it('does not call onUpdate handler if status is 304', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse1 = getMockAbortableRequest( + Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: { 'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT' } })); + const mockResponse2 = getMockAbortableRequest( + Promise.resolve({ statusCode: 304, body: '{"foo2": "bar2"}', headers: {} })); + const mockResponse3 = getMockAbortableRequest( + Promise.resolve({ statusCode: 200, body: '{"foo3": "bar3"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse1) + .mockReturnValueOnce(mockResponse2).mockReturnValueOnce(mockResponse3); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatDoesNotExists', + autoUpdate: true, + }); + + manager.start(); + + const listener = vi.fn(); + manager.onUpdate(listener); + + for(let i = 0; i <3; i++) { + await repeater.execute(0); + } + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(listener).toHaveBeenCalledTimes(2); + expect(listener).not.toHaveBeenCalledWith('{"foo2": "bar2"}'); + expect(listener).toHaveBeenNthCalledWith(1, '{"foo": "bar"}'); + expect(listener).toHaveBeenNthCalledWith(2, '{"foo3": "bar3"}'); + }); + }); + + it('sends the access token in the request Authorization header', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + datafileAccessToken: 'token123', + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(requestHandler.makeRequest.mock.calls[0][1].Authorization).toBe('Bearer token123'); + }); + + it('uses the provided urlTemplate', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + urlTemplate: 'https://example.com/datafile?key=%s', + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(requestHandler.makeRequest.mock.calls[0][0]).toBe('https://example.com/datafile?key=keyThatExists'); + }); + + it('uses the default urlTemplate if none is provided and datafileAccessToken is also not provided', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(requestHandler.makeRequest.mock.calls[0][0]).toBe(DEFAULT_URL_TEMPLATE.replace('%s', 'keyThatExists')); + }); + + it('uses the default authenticated urlTemplate if none is provided and datafileAccessToken is provided', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + datafileAccessToken: 'token123', + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + expect(requestHandler.makeRequest.mock.calls[0][0]).toBe(DEFAULT_AUTHENTICATED_URL_TEMPLATE.replace('%s', 'keyThatExists')); + }); + + it('returns the datafile from get', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.get()).toBe('{"foo": "bar"}'); + }); + + it('returns undefined from get before becoming ready', () => { + const repeater = getMockRepeater(); + const mockResponse = getMockAbortableRequest(); + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + }); + manager.start(); + expect(manager.get()).toBeUndefined(); + }); + + it('removes the onUpdate handler when the retuned function is called', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValue(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + const listener = vi.fn(); + const removeListener = manager.onUpdate(listener); + + manager.start(); + repeater.execute(0); + + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(listener).toHaveBeenCalledTimes(1); + removeListener(); + + await repeater.execute(0); + expect(listener).toHaveBeenCalledTimes(1); + }); + + describe('stop', () => { + it('rejects onRunning when stop is called if manager state is New', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + expect(manager.getState()).toBe(ServiceState.New); + manager.stop(); + await expect(manager.onRunning()).rejects.toThrow(); + }); + + it('rejects onRunning when stop is called if manager state is Starting', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + manager.start(); + expect(manager.getState()).toBe(ServiceState.Starting); + manager.stop(); + await expect(manager.onRunning()).rejects.toThrow(); + }); + + it('stops the repeater, set state to Termimated, and resolve onTerminated when stop is called', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + manager.start(); + await repeater.execute(0); + await expect(manager.onRunning()).resolves.not.toThrow(); + + manager.stop(); + await expect(manager.onTerminated()).resolves.not.toThrow(); + expect(repeater.stop).toHaveBeenCalled(); + expect(manager.getState()).toBe(ServiceState.Terminated); + }); + + it('aborts the current request if stop is called', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + manager.start(); + repeater.execute(0); + + expect(requestHandler.makeRequest).toHaveBeenCalledOnce(); + manager.stop(); + expect(mockResponse.abort).toHaveBeenCalled(); + }); + + it('does not call onUpdate handler after stop is called', async () => { + const repeater = getMockRepeater(); + const requestHandler = getMockRequestHandler(); + const mockResponse = getMockAbortableRequest(Promise.resolve({ statusCode: 200, body: '{"foo": "bar"}', headers: {} })); + requestHandler.makeRequest.mockReturnValueOnce(mockResponse); + + const manager = new PollingDatafileManager({ + repeater, + requestHandler, + sdkKey: 'keyThatExists', + autoUpdate: true, + }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + repeater.execute(0); + manager.stop(); + + expect(listener).not.toHaveBeenCalled(); + }); + }) +}); diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts new file mode 100644 index 000000000..ba8e70139 --- /dev/null +++ b/lib/project_config/polling_datafile_manager.ts @@ -0,0 +1,262 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { sprintf } from '../utils/fns'; +import { DatafileManager, DatafileManagerConfig } from './datafile_manager'; +import { EventEmitter } from '../utils/event_emitter/event_emitter'; +import { DEFAULT_AUTHENTICATED_URL_TEMPLATE, DEFAULT_URL_TEMPLATE } from './constant'; +import { Store } from '../utils/cache/store'; +import { BaseService, ServiceState } from '../service'; +import { RequestHandler, AbortableRequest, Headers, Response } from '../utils/http_request_handler/http'; +import { Repeater } from '../utils/repeater/repeater'; +import { Consumer, Fn } from '../utils/type'; +import { isSuccessStatusCode } from '../utils/http_request_handler/http_util'; +import { + DATAFILE_FETCH_REQUEST_FAILED, + ERROR_FETCHING_DATAFILE, +} from 'error_message'; +import { + ADDING_AUTHORIZATION_HEADER_WITH_BEARER_TOKEN, + MAKING_DATAFILE_REQ_TO_URL_WITH_HEADERS, + RESPONSE_STATUS_CODE, + SAVED_LAST_MODIFIED_HEADER_VALUE_FROM_RESPONSE, +} from 'log_message'; +import { OptimizelyError } from '../error/optimizly_error'; +import { LoggerFacade } from '../logging/logger'; + +export const LOGGER_NAME = 'PollingDatafileManager'; + +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; + +export const FAILED_TO_FETCH_DATAFILE = 'Failed to fetch datafile'; + +export class PollingDatafileManager extends BaseService implements DatafileManager { + private requestHandler: RequestHandler; + private currentDatafile?: string; + private emitter: EventEmitter<{ update: string }>; + private autoUpdate: boolean; + private initRetryRemaining?: number; + private repeater: Repeater; + private lastResponseLastModified?: string; + private datafileUrl: string; + private currentRequest?: AbortableRequest; + private cacheKey: string; + private cache?: Store<string>; + private sdkKey: string; + private datafileAccessToken?: string; + + constructor(config: DatafileManagerConfig) { + super(config.startupLogs); + const { + autoUpdate = false, + sdkKey, + datafileAccessToken, + urlTemplate, + cache, + initRetry, + repeater, + requestHandler, + logger, + } = config; + this.cache = cache; + this.cacheKey = 'opt-datafile-' + sdkKey; + this.sdkKey = sdkKey; + this.datafileAccessToken = datafileAccessToken; + this.requestHandler = requestHandler; + this.emitter = new EventEmitter(); + this.autoUpdate = autoUpdate; + this.initRetryRemaining = initRetry; + this.repeater = repeater; + + if (logger) { + this.setLogger(logger); + } + + const urlTemplateToUse = urlTemplate || + (datafileAccessToken ? DEFAULT_AUTHENTICATED_URL_TEMPLATE : DEFAULT_URL_TEMPLATE); + this.datafileUrl = sprintf(urlTemplateToUse, this.sdkKey); + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + } + + onUpdate(listener: Consumer<string>): Fn { + return this.emitter.on('update', listener); + } + + get(): string | undefined { + return this.currentDatafile; + } + + start(): void { + if (!this.isNew()) { + return; + } + + super.start(); + this.state = ServiceState.Starting; + this.setDatafileFromCacheIfAvailable(); + this.repeater.setTask(this.syncDatafile.bind(this)); + this.repeater.start(true); + } + + makeDisposable(): void { + super.makeDisposable(); + this.initRetryRemaining = Math.min(this.initRetryRemaining ?? 5, 5); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (this.isNew() || this.isStarting()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'PollingDatafileManager') + )); + } + + this.state = ServiceState.Terminated; + this.repeater.stop(); + this.currentRequest?.abort(); + this.emitter.removeAllListeners(); + this.stopPromise.resolve(); + } + + private handleInitFailure(): void { + this.state = ServiceState.Failed; + this.repeater.stop(); + const error = new Error(FAILED_TO_FETCH_DATAFILE); + this.startPromise.reject(error); + this.stopPromise.reject(error); + } + + private handleError(errorOrStatus: Error | number): void { + if (this.isDone()) { + return; + } + + if (errorOrStatus instanceof Error) { + this.logger?.error(ERROR_FETCHING_DATAFILE, errorOrStatus.message, errorOrStatus); + } else { + this.logger?.error(DATAFILE_FETCH_REQUEST_FAILED, errorOrStatus); + } + + if(this.isStarting() && this.initRetryRemaining !== undefined) { + if (this.initRetryRemaining === 0) { + this.handleInitFailure(); + } else { + this.initRetryRemaining--; + } + } + } + + private async onRequestRejected(err: any): Promise<void> { + this.handleError(err); + return Promise.reject(err); + } + + private async onRequestResolved(response: Response): Promise<void> { + if (this.isDone()) { + return; + } + + this.saveLastModified(response.headers); + + if (!isSuccessStatusCode(response.statusCode)) { + this.handleError(response.statusCode); + return Promise.reject(new Error()); + } + + const datafile = this.getDatafileFromResponse(response); + if (datafile) { + this.handleDatafile(datafile); + // if autoUpdate is off, don't need to sync datafile any more + // if disposable, stop the repeater after the first successful fetch + if (!this.autoUpdate || this.disposable) { + this.repeater.stop(); + } + } + } + + private makeDatafileRequest(): AbortableRequest { + const headers: Headers = {}; + if (this.lastResponseLastModified) { + headers['if-modified-since'] = this.lastResponseLastModified; + } + + if (this.datafileAccessToken) { + this.logger?.debug(ADDING_AUTHORIZATION_HEADER_WITH_BEARER_TOKEN); + headers['Authorization'] = `Bearer ${this.datafileAccessToken}`; + } + + this.logger?.debug(MAKING_DATAFILE_REQ_TO_URL_WITH_HEADERS, this.datafileUrl, () => JSON.stringify(headers)); + return this.requestHandler.makeRequest(this.datafileUrl, headers, 'GET'); + } + + private async syncDatafile(): Promise<void> { + this.currentRequest = this.makeDatafileRequest(); + return this.currentRequest.responsePromise + .then(this.onRequestResolved.bind(this), this.onRequestRejected.bind(this)) + .finally(() => this.currentRequest = undefined); + } + + private handleDatafile(datafile: string): void { + if (this.isDone()) { + return; + } + + this.currentDatafile = datafile; + this.cache?.set(this.cacheKey, datafile); + + if (this.isStarting()) { + this.startPromise.resolve(); + this.state = ServiceState.Running; + } + this.emitter.emit('update', datafile); + } + + private getDatafileFromResponse(response: Response): string | undefined{ + this.logger?.debug(RESPONSE_STATUS_CODE, response.statusCode); + if (response.statusCode === 304) { + return undefined; + } + return response.body; + } + + private saveLastModified(headers: Headers): void { + const lastModifiedHeader = headers['last-modified'] || headers['Last-Modified']; + if (lastModifiedHeader !== undefined) { + this.lastResponseLastModified = lastModifiedHeader; + this.logger?.debug(SAVED_LAST_MODIFIED_HEADER_VALUE_FROM_RESPONSE, this.lastResponseLastModified); + } + } + + private async setDatafileFromCacheIfAvailable(): Promise<void> { + if (!this.cache) { + return; + } + try { + const datafile = await this.cache.get(this.cacheKey); + if (datafile && this.isStarting()) { + this.handleDatafile(datafile); + } + } catch { + // ignore error + } + } +} diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts new file mode 100644 index 000000000..21d18940c --- /dev/null +++ b/lib/project_config/project_config.spec.ts @@ -0,0 +1,1354 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock, beforeAll, afterAll } from 'vitest'; +import { sprintf } from '../utils/fns'; +import { keyBy } from '../utils/fns'; +import projectConfig, { ProjectConfig, getHoldoutsForFlag } from './project_config'; +import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; +import testDatafile from '../tests/test_data'; +import configValidator from '../utils/config_validator'; +import { + INVALID_EXPERIMENT_ID, + INVALID_EXPERIMENT_KEY, + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + UNRECOGNIZED_ATTRIBUTE, + VARIABLE_KEY_NOT_IN_DATAFILE, + FEATURE_NOT_IN_DATAFILE, + UNABLE_TO_CAST_VALUE, +} from 'error_message'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { VariableType } from '../shared_types'; +import { OptimizelyError } from '../error/optimizly_error'; +import { mock } from 'node:test'; + +const buildLogMessageFromArgs = (args: any[]) => sprintf(args[1], ...args.splice(2)); +const cloneDeep = (obj: any) => JSON.parse(JSON.stringify(obj)); +const logger = getMockLogger(); + +const mockHoldoutToggle = vi.hoisted(() => vi.fn()); + +vi.mock('../feature_toggle', () => { + return { + holdout: mockHoldoutToggle, + }; +}); + +describe('createProjectConfig', () => { + let configObj: ProjectConfig; + + it('should use US region when no region is specified in datafile', () => { + const datafile = testDatafile.getTestProjectConfig(); + const config = projectConfig.createProjectConfig(datafile); + + expect(config.region).toBe('US'); + }); + + it('should parse region specified in datafile correctly', () => { + const datafileUs = testDatafile.getTestProjectConfig(); + datafileUs.region = 'US'; + + const configUs = projectConfig.createProjectConfig(datafileUs); + expect(configUs.region).toBe('US'); + + const datafileEu = testDatafile.getTestProjectConfig(); + datafileEu.region = 'EU'; + const configEu = projectConfig.createProjectConfig(datafileEu); + + expect(configEu.region).toBe('EU'); + }); + + it('should set properties correctly when createProjectConfig is called', () => { + const testData: Record<string, any> = testDatafile.getTestProjectConfig(); + configObj = projectConfig.createProjectConfig(testData as JSON); + + testData.audiences.forEach((audience: any) => { + audience.conditions = JSON.parse(audience.conditions); + }); + + expect(configObj.accountId).toBe(testData.accountId); + expect(configObj.projectId).toBe(testData.projectId); + expect(configObj.revision).toBe(testData.revision); + expect(configObj.events).toEqual(testData.events); + expect(configObj.audiences).toEqual(testData.audiences); + + testData.groups.forEach((group: any) => { + group.experiments.forEach((experiment: any) => { + experiment.groupId = group.id; + experiment.variationKeyMap = keyBy(experiment.variations, 'key'); + }); + }); + + expect(configObj.groups).toEqual(testData.groups); + + const expectedGroupIdMap = { + 666: testData.groups[0], + 667: testData.groups[1], + }; + + expect(configObj.groupIdMap).toEqual(expectedGroupIdMap); + + const expectedExperiments = testData.experiments.slice(); + + Object.entries(configObj.groupIdMap).forEach(([groupId, group]) => { + group.experiments.forEach((experiment: any) => { + experiment.groupId = groupId; + expectedExperiments.push(experiment); + }); + }) + expectedExperiments.forEach((experiment: any) => { + experiment.variationKeyMap = keyBy(experiment.variations, 'key'); + }) + + expect(configObj.experiments).toEqual(expectedExperiments); + + const expectedAttributeKeyMap = { + browser_type: testData.attributes[0], + boolean_key: testData.attributes[1], + integer_key: testData.attributes[2], + double_key: testData.attributes[3], + valid_positive_number: testData.attributes[4], + valid_negative_number: testData.attributes[5], + invalid_number: testData.attributes[6], + array: testData.attributes[7], + }; + + expect(configObj.attributeKeyMap).toEqual(expectedAttributeKeyMap); + + const expectedExperimentKeyMap = { + testExperiment: configObj.experiments[0], + testExperimentWithAudiences: configObj.experiments[1], + testExperimentNotRunning: configObj.experiments[2], + testExperimentLaunched: configObj.experiments[3], + groupExperiment1: configObj.experiments[4], + groupExperiment2: configObj.experiments[5], + overlappingGroupExperiment1: configObj.experiments[6], + }; + + expect(configObj.experimentKeyMap).toEqual(expectedExperimentKeyMap); + + const expectedEventKeyMap = { + testEvent: testData.events[0], + 'Total Revenue': testData.events[1], + testEventWithAudiences: testData.events[2], + testEventWithoutExperiments: testData.events[3], + testEventWithExperimentNotRunning: testData.events[4], + testEventWithMultipleExperiments: testData.events[5], + testEventLaunched: testData.events[6], + }; + + expect(configObj.eventKeyMap).toEqual(expectedEventKeyMap); + + const expectedExperimentIdMap = { + '111127': configObj.experiments[0], + '122227': configObj.experiments[1], + '133337': configObj.experiments[2], + '144447': configObj.experiments[3], + '442': configObj.experiments[4], + '443': configObj.experiments[5], + '444': configObj.experiments[6], + }; + + expect(configObj.experimentIdMap).toEqual(expectedExperimentIdMap); + }); + + it('should not mutate the datafile', () => { + const datafile = testDatafile.getTypedAudiencesConfig(); + const datafileClone = cloneDeep(datafile); + projectConfig.createProjectConfig(datafile as any); + + expect(datafile).toEqual(datafileClone); + }); +}); + +describe('createProjectConfig - feature management', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + it('should create a rolloutIdMap from rollouts in the datafile', () => { + expect(configObj.rolloutIdMap).toEqual(testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); + }); + + it('should create a variationVariableUsageMap from rollouts and experiments with features in the datafile', () => { + expect(configObj.variationVariableUsageMap).toEqual( + testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap + ); + }); + + it('should create a featureKeyMap from features in the datafile', () => { + expect(configObj.featureKeyMap).toEqual(testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); + }); + + it('should add variations from rollout experiements to the variationKeyMap', () => { + expect(configObj.variationIdMap['594032']).toEqual({ + variables: [ + { value: 'true', id: '4919852825313280' }, + { value: '395', id: '5482802778734592' }, + { value: '4.99', id: '6045752732155904' }, + { value: 'Hello audience', id: '6327227708866560' }, + { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' }, + ], + featureEnabled: true, + key: '594032', + id: '594032', + }); + + expect(configObj.variationIdMap['594038']).toEqual({ + variables: [ + { value: 'false', id: '4919852825313280' }, + { value: '400', id: '5482802778734592' }, + { value: '14.99', id: '6045752732155904' }, + { value: 'Hello', id: '6327227708866560' }, + { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' }, + ], + featureEnabled: false, + key: '594038', + id: '594038', + }); + + expect(configObj.variationIdMap['594061']).toEqual({ + variables: [ + { value: '27.34', id: '5060590313668608' }, + { value: 'Winter is NOT coming', id: '5342065290379264' }, + { value: '10003', id: '6186490220511232' }, + { value: 'false', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594061', + id: '594061', + }); + + expect(configObj.variationIdMap['594067']).toEqual({ + variables: [ + { value: '30.34', id: '5060590313668608' }, + { value: 'Winter is coming definitely', id: '5342065290379264' }, + { value: '500', id: '6186490220511232' }, + { value: 'true', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594067', + id: '594067', + }); + }); +}); + +describe('createProjectConfig - flag variations', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); + }); + + it('should populate flagVariationsMap correctly', function() { + const allVariationsForFlag = configObj.flagVariationsMap; + const feature1Variations = allVariationsForFlag.feature_1; + const feature2Variations = allVariationsForFlag.feature_2; + const feature3Variations = allVariationsForFlag.feature_3; + const feature1VariationsKeys = feature1Variations.map(variation => { + return variation.key; + }, {}); + const feature2VariationsKeys = feature2Variations.map(variation => { + return variation.key; + }, {}); + const feature3VariationsKeys = feature3Variations.map(variation => { + return variation.key; + }, {}); + + expect(feature1VariationsKeys).toEqual(['a', 'b', '3324490633', '3324490562', '18257766532']); + expect(feature2VariationsKeys).toEqual(['variation_with_traffic', 'variation_no_traffic']); + expect(feature3VariationsKeys).toEqual([]); + }); +}); + +describe('createProjectConfig - cmab experiments', () => { + it('should populate cmab field correctly', function() { + const datafile = testDatafile.getTestProjectConfig(); + datafile.experiments[0].cmab = { + attributeIds: ['808797688', '808797689'], + trafficAllocation: 3141, + }; + + datafile.experiments[2].cmab = { + attributeIds: ['808797689'], + trafficAllocation: 1414, + }; + + const configObj = projectConfig.createProjectConfig(datafile); + + const experiment0 = configObj.experiments[0]; + expect(experiment0.cmab).toEqual({ + trafficAllocation: 3141, + attributeIds: ['808797688', '808797689'], + }); + + const experiment1 = configObj.experiments[1]; + expect(experiment1.cmab).toBeUndefined(); + + const experiment2 = configObj.experiments[2]; + expect(experiment2.cmab).toEqual({ + trafficAllocation: 1414, + attributeIds: ['808797689'], + }); + }); +}); + +const getHoldoutDatafile = () => { + const datafile = testDatafile.getTestDecideProjectConfig(); + + // Add holdouts to the datafile + datafile.holdouts = [ + { + id: 'holdout_id_1', + key: 'holdout_1', + status: 'Running', + includedFlags: [], + excludedFlags: [], + audienceIds: ['13389130056'], + audienceConditions: ['or', '13389130056'], + variations: [ + { + id: 'var_id_1', + key: 'holdout_variation_1', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'var_id_1', + endOfRange: 5000 + } + ] + }, + { + id: 'holdout_id_2', + key: 'holdout_2', + status: 'Running', + includedFlags: [], + excludedFlags: ['44829230000'], + audienceIds: [], + audienceConditions: [], + variations: [ + { + id: 'var_id_2', + key: 'holdout_variation_2', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'var_id_2', + endOfRange: 1000 + } + ] + }, + { + id: 'holdout_id_3', + key: 'holdout_3', + status: 'Draft', + includedFlags: ['4482920077'], + excludedFlags: [], + audienceIds: [], + audienceConditions: [], + variations: [ + { + id: 'var_id_2', + key: 'holdout_variation_2', + variables: [] + } + ], + trafficAllocation: [ + { + entityId: 'var_id_2', + endOfRange: 1000 + } + ] + } + ]; + + return datafile; +} + +describe('createProjectConfig - holdouts, feature toggle is on', () => { + beforeAll(() => { + mockHoldoutToggle.mockReturnValue(true); + }); + + afterAll(() => { + mockHoldoutToggle.mockReset(); + }); + + it('should populate holdouts fields correctly', function() { + const datafile = getHoldoutDatafile(); + + mockHoldoutToggle.mockReturnValue(true); + + const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile))); + + expect(configObj.holdouts).toHaveLength(3); + configObj.holdouts.forEach((holdout, i) => { + expect(holdout).toEqual(expect.objectContaining(datafile.holdouts[i])); + expect(holdout.variationKeyMap).toEqual( + keyBy(datafile.holdouts[i].variations, 'key') + ); + }); + + expect(configObj.holdoutIdMap).toEqual({ + holdout_id_1: configObj.holdouts[0], + holdout_id_2: configObj.holdouts[1], + holdout_id_3: configObj.holdouts[2], + }); + + expect(configObj.globalHoldouts).toHaveLength(2); + expect(configObj.globalHoldouts).toEqual([ + configObj.holdouts[0], // holdout_1 has empty includedFlags + configObj.holdouts[1] // holdout_2 has empty includedFlags + ]); + + expect(configObj.includedHoldouts).toEqual({ + feature_1: [configObj.holdouts[2]], // holdout_3 includes feature_1 (ID: 4482920077) + }); + + expect(configObj.excludedHoldouts).toEqual({ + feature_3: [configObj.holdouts[1]] // holdout_2 excludes feature_3 (ID: 44829230000) + }); + + expect(configObj.flagHoldoutsMap).toEqual({}); + }); + + it('should handle empty holdouts array', function() { + const datafile = testDatafile.getTestProjectConfig(); + + const configObj = projectConfig.createProjectConfig(datafile); + + expect(configObj.holdouts).toEqual([]); + expect(configObj.holdoutIdMap).toEqual({}); + expect(configObj.globalHoldouts).toEqual([]); + expect(configObj.includedHoldouts).toEqual({}); + expect(configObj.excludedHoldouts).toEqual({}); + expect(configObj.flagHoldoutsMap).toEqual({}); + }); + + it('should handle undefined includedFlags and excludedFlags in holdout', function() { + const datafile = getHoldoutDatafile(); + datafile.holdouts[0].includedFlags = undefined; + datafile.holdouts[0].excludedFlags = undefined; + + const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile))); + + expect(configObj.holdouts).toHaveLength(3); + expect(configObj.holdouts[0].includedFlags).toEqual([]); + expect(configObj.holdouts[0].excludedFlags).toEqual([]); + }); +}); + +describe('getHoldoutsForFlag: feature toggle is on', () => { + beforeAll(() => { + mockHoldoutToggle.mockReturnValue(true); + }); + + afterAll(() => { + mockHoldoutToggle.mockReset(); + }); + + it('should return all applicable holdouts for a flag', () => { + const datafile = getHoldoutDatafile(); + const configObj = projectConfig.createProjectConfig(JSON.parse(JSON.stringify(datafile))); + + const feature1Holdouts = getHoldoutsForFlag(configObj, 'feature_1'); + expect(feature1Holdouts).toHaveLength(3); + expect(feature1Holdouts).toEqual([ + configObj.holdouts[0], + configObj.holdouts[1], + configObj.holdouts[2], + ]); + + const feature2Holdouts = getHoldoutsForFlag(configObj, 'feature_2'); + expect(feature2Holdouts).toHaveLength(2); + expect(feature2Holdouts).toEqual([ + configObj.holdouts[0], + configObj.holdouts[1], + ]); + + const feature3Holdouts = getHoldoutsForFlag(configObj, 'feature_3'); + expect(feature3Holdouts).toHaveLength(1); + expect(feature3Holdouts).toEqual([ + configObj.holdouts[0], + ]); + }); +}); + +describe('getExperimentId', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + let createdLogger: ReturnType<typeof getMockLogger>; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + createdLogger = getMockLogger(); + }); + + it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { + expect(projectConfig.getExperimentId(configObj, testData.experiments[0].key)).toBe(testData.experiments[0].id); + }); + + it('should throw error for invalid experiment key in getExperimentId', function() { + expect(() => { + projectConfig.getExperimentId(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_KEY, + params: ['invalidExperimentId'], + }) + ); + }); +}); + +describe('getLayerId', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve layer ID for valid experiment key in getLayerId', function() { + expect(projectConfig.getLayerId(configObj, '111127')).toBe('4'); + }); + + it('should throw error for invalid experiment key in getLayerId', function() { + expect(() => projectConfig.getLayerId(configObj, 'invalidExperimentKey')).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentKey'], + }) + ); + }); +}); + +describe('getAttributeId', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + let createdLogger: ReturnType<typeof getMockLogger>; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + createdLogger = getMockLogger(); + }); + + it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { + expect(projectConfig.getAttributeId(configObj, 'browser_type')).toBe('111094'); + }); + + it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { + expect(projectConfig.getAttributeId(configObj, '$opt_user_agent')).toBe('$opt_user_agent'); + }); + + it('should return null for invalid attribute key in getAttributeId', function() { + expect(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)).toBe(null); + expect(createdLogger.warn).toHaveBeenCalledWith(UNRECOGNIZED_ATTRIBUTE, 'invalidAttributeKey'); + }); + + it('should return null for invalid attribute key in getAttributeId', () => { + // Adding attribute in key map with reserved prefix + configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { + id: '42', + }; + + expect(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger)).toBe('42'); + expect(createdLogger.warn).toHaveBeenCalledWith( + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + '$opt_some_reserved_attribute', + '$opt_' + ); + }); +}); + +describe('getEventId', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve event ID for valid event key in getEventId', function() { + expect(projectConfig.getEventId(configObj, 'testEvent')).toBe('111095'); + }); + + it('should return null for invalid event key in getEventId', function() { + expect(projectConfig.getEventId(configObj, 'invalidEventKey')).toBe(null); + }); +}); + +describe('getExperimentStatus', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { + expect(projectConfig.getExperimentStatus(configObj, testData.experiments[0].key)).toBe( + testData.experiments[0].status + ); + }); + + it('should throw error for invalid experiment key in getExperimentStatus', function() { + expect(() => { + projectConfig.getExperimentStatus(configObj, 'invalidExeprimentKey'); + }).toThrowError(OptimizelyError); + }); +}); + +describe('isActive', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return true if experiment status is set to Running in isActive', function() { + expect(projectConfig.isActive(configObj, 'testExperiment')).toBe(true); + }); + + it('should return false if experiment status is not set to Running in isActive', function() { + expect(projectConfig.isActive(configObj, 'testExperimentNotRunning')).toBe(false); + }); +}); + +describe('isRunning', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(() => { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return true if experiment status is set to Running in isRunning', function() { + expect(projectConfig.isRunning(configObj, 'testExperiment')).toBe(true); + }); + + it('should return false if experiment status is not set to Running in isRunning', function() { + expect(projectConfig.isRunning(configObj, 'testExperimentLaunched')).toBe(false); + }); +}); + +describe('getVariationKeyFromId', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { + expect(projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id)).toBe( + testData.experiments[0].variations[0].key + ); + }); +}); + +describe('getTrafficAllocation', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + expect(projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id)).toEqual( + testData.experiments[0].trafficAllocation + ); + }); + + it('should throw error for invalid experient key in getTrafficAllocation', function() { + expect(() => { + projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentId'], + }) + ); + }); +}); + +describe('getVariationIdFromExperimentAndVariationKey', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return the variation id for the given experiment key and variation key', () => { + expect( + projectConfig.getVariationIdFromExperimentAndVariationKey( + configObj, + testData.experiments[0].key, + testData.experiments[0].variations[0].key + ) + ).toBe(testData.experiments[0].variations[0].id); + }); +}); + +describe('getSendFlagDecisionsValue', () => { + let testData: Record<string, any>; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should return false when sendFlagDecisions is undefined', () => { + configObj.sendFlagDecisions = undefined; + + expect(projectConfig.getSendFlagDecisionsValue(configObj)).toBe(false); + }); + + it('should return false when sendFlagDecisions is set to false', () => { + configObj.sendFlagDecisions = false; + + expect(projectConfig.getSendFlagDecisionsValue(configObj)).toBe(false); + }); + + it('should return true when sendFlagDecisions is set to true', () => { + configObj.sendFlagDecisions = true; + + expect(projectConfig.getSendFlagDecisionsValue(configObj)).toBe(true); + }); +}); + +describe('getVariableForFeature', function() { + let featureManagementLogger: ReturnType<typeof getMockLogger>; + let configObj: ProjectConfig; + + beforeEach(() => { + featureManagementLogger = getMockLogger(); + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return a variable object for a valid variable and feature key', function() { + const featureKey = 'test_feature_for_experiment'; + const variableKey = 'num_buttons'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toEqual({ + type: 'integer', + key: 'num_buttons', + id: '4792309476491264', + defaultValue: '10', + }); + }); + + it('should return null for an invalid variable key and a valid feature key', function() { + const featureKey = 'test_feature_for_experiment'; + const variableKey = 'notARealVariable____'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledOnce(); + expect(featureManagementLogger.error).toHaveBeenCalledWith( + VARIABLE_KEY_NOT_IN_DATAFILE, + 'notARealVariable____', + 'test_feature_for_experiment' + ); + }); + + it('should return null for an invalid feature key', function() { + const featureKey = 'notARealFeature_____'; + const variableKey = 'num_buttons'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledOnce(); + expect(featureManagementLogger.error).toHaveBeenCalledWith(FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____'); + }); + + it('should return null for an invalid variable key and an invalid feature key', function() { + const featureKey = 'notARealFeature_____'; + const variableKey = 'notARealVariable____'; + const result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledOnce(); + expect(featureManagementLogger.error).toHaveBeenCalledWith(FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____'); + }); +}); + +describe('getVariableValueForVariation', () => { + let featureManagementLogger: ReturnType<typeof getMockLogger>; + let configObj: ProjectConfig; + + beforeEach(() => { + featureManagementLogger = getMockLogger(); + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return a value for a valid variation and variable', () => { + const variation = configObj.variationIdMap['594096']; + let variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + let result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + expect(result).toBe('2'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe('true'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe('Buy me NOW'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe('20.25'); + }); + + it('should return null for a null variation', () => { + const variation = null; + const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('should return null for a null variable', () => { + const variation = configObj.variationIdMap['594096']; + const variable = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('should return null for a null variation and null variable', () => { + const variation = null; + const variable = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('should return null for a variation whose id is not in the datafile', () => { + const variation = { + key: 'some_variation', + id: '999999999999', + variables: [], + }; + const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); + + it('should return null if the variation does not have a value for this variable', () => { + const variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it + const variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + const result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + + expect(result).toBe(null); + }); +}); + +describe('getTypeCastValue', () => { + let featureManagementLogger: ReturnType<typeof getMockLogger>; + let configObj: ProjectConfig; + + beforeEach(() => { + featureManagementLogger = getMockLogger(); + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should cast a boolean', () => { + let result = projectConfig.getTypeCastValue( + 'true', + FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, + featureManagementLogger + ); + + expect(result).toBe(true); + + result = projectConfig.getTypeCastValue( + 'false', + FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, + featureManagementLogger + ); + + expect(result).toBe(false); + }); + + it('should cast an integer', () => { + let result = projectConfig.getTypeCastValue( + '50', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(50); + + result = projectConfig.getTypeCastValue( + '-7', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(-7); + + result = projectConfig.getTypeCastValue( + '0', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(0); + }); + + it('should cast a double', () => { + let result = projectConfig.getTypeCastValue( + '89.99', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(89.99); + + result = projectConfig.getTypeCastValue( + '-257.21', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(-257.21); + + result = projectConfig.getTypeCastValue( + '0', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(0); + + result = projectConfig.getTypeCastValue( + '10', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(10); + }); + + it('should return a string unmodified', () => { + const result = projectConfig.getTypeCastValue( + 'message', + FEATURE_VARIABLE_TYPES.STRING as VariableType, + featureManagementLogger + ); + + expect(result).toBe('message'); + }); + + it('should return null and logs an error for an invalid boolean', () => { + const result = projectConfig.getTypeCastValue( + 'notabool', + FEATURE_VARIABLE_TYPES.BOOLEAN as VariableType, + featureManagementLogger + ); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notabool', 'boolean'); + }); + + it('should return null and logs an error for an invalid integer', () => { + const result = projectConfig.getTypeCastValue( + 'notanint', + FEATURE_VARIABLE_TYPES.INTEGER as VariableType, + featureManagementLogger + ); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notanint', 'integer'); + }); + + it('should return null and logs an error for an invalid double', () => { + const result = projectConfig.getTypeCastValue( + 'notadouble', + FEATURE_VARIABLE_TYPES.DOUBLE as VariableType, + featureManagementLogger + ); + + expect(result).toBe(null); + expect(featureManagementLogger.error).toHaveBeenCalledWith(UNABLE_TO_CAST_VALUE, 'notadouble', 'double'); + }); +}); + +describe('getAudiencesById', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + }); + + it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', () => { + expect(projectConfig.getAudiencesById(configObj)).toEqual(testDatafile.typedAudiencesById); + }); +}); + +describe('getExperimentAudienceConditions', () => { + let configObj: ProjectConfig; + let testData: Record<string, any>; + + beforeEach(() => { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + }); + + it('should retrieve audiences for valid experiment key', () => { + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + + expect(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id)).toEqual(['11154']); + }); + + it('should throw error for invalid experiment key', () => { + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + + expect(() => { + projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentId'], + }) + ); + }); + + it('should return experiment audienceIds if experiment has no audienceConditions', () => { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + const result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); + + expect(result).toEqual([ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ]); + }); + + it('should return experiment audienceConditions if experiment has audienceConditions', () => { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + // audience_combinations_experiment has both audienceConditions and audienceIds + // audienceConditions should be preferred over audienceIds + const result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); + + expect(result).toEqual([ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ]); + }); +}); + +describe('isFeatureExperiment', () => { + it('should return true for a feature test', () => { + const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + const result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' + + expect(result).toBe(true); + }); + + it('should return false for an A/B test', () => { + const config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + const result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + + expect(result).toBe(false); + }); + + it('should return true for a feature test in a mutex group', () => { + const config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + let result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + + expect(result).toBe(true); + + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + + expect(result).toBe(true); + }); +}); + +describe('getAudienceSegments', () => { + it('should return all qualified segments from an audience', () => { + const dummyQualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); + + expect(dummyQualifiedAudienceJsonSegments).toEqual(['odp-segment-1']); + + const dummyUnqualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'invalid', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); + + expect(dummyUnqualifiedAudienceJsonSegments).toEqual([]); + }); +}); + +describe('integrations: with segments', () => { + let configObj: ProjectConfig; + + beforeEach(() => { + configObj = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + expect(configObj.integrations).toBeDefined(); + expect(configObj.integrations.length).toBe(4); + }); + + it('should populate odpIntegrationConfig', () => { + expect(configObj.odpIntegrationConfig.integrated).toBe(true); + + assert(configObj.odpIntegrationConfig.integrated); + + expect(configObj.odpIntegrationConfig.odpConfig.apiKey).toBe('W4WzcEs-ABgXorzY7h1LCQ'); + expect(configObj.odpIntegrationConfig.odpConfig.apiHost).toBe('https://api.zaius.com'); + expect(configObj.odpIntegrationConfig.odpConfig.pixelUrl).toBe('https://jumbe.zaius.com'); + expect(configObj.odpIntegrationConfig.odpConfig.segmentsToCheck).toEqual([ + 'odp-segment-1', + 'odp-segment-2', + 'odp-segment-3', + ]); + }); +}); + +describe('integrations: without segments', () => { + let config: ProjectConfig; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + expect(config.integrations).toBeDefined(); + expect(config.integrations.length).toBe(3); + }); + + it('should populate odpIntegrationConfig', () => { + expect(config.odpIntegrationConfig.integrated).toBe(true); + + assert(config.odpIntegrationConfig.integrated); + + expect(config.odpIntegrationConfig.odpConfig.apiKey).toBe('W4WzcEs-ABgXorzY7h1LCQ'); + expect(config.odpIntegrationConfig.odpConfig.apiHost).toBe('https://api.zaius.com'); + expect(config.odpIntegrationConfig.odpConfig.pixelUrl).toBe('https://jumbe.zaius.com'); + expect(config.odpIntegrationConfig.odpConfig.segmentsToCheck).toEqual([]); + }); +}); + +describe('without valid integration key', () => { + it('should throw an error when parsing the project config due to integrations not containing a key', () => { + const odpIntegratedConfigWithoutKey = testDatafile.getOdpIntegratedConfigWithoutKey(); + + expect(() => projectConfig.createProjectConfig(odpIntegratedConfigWithoutKey)).toThrowError(OptimizelyError); + }); +}); + +describe('without integrations', () => { + let config: ProjectConfig; + + beforeEach(() => { + const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); + const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; + config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + }); + + it('should convert integrations from the datafile into the project config', () => { + expect(config.integrations.length).toBe(0); + }); + + it('should populate odpIntegrationConfig', () => { + expect(config.odpIntegrationConfig.integrated).toBe(false); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(config.odpIntegrationConfig.odpConfig).toBeUndefined(); + }); +}); + +describe('tryCreatingProjectConfig', () => { + let mockJsonSchemaValidator: Mock; + beforeEach(() => { + mockJsonSchemaValidator = vi.fn().mockReturnValue(true); + vi.spyOn(configValidator, 'validateDatafile').mockReturnValue(true); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return a project config object created by createProjectConfig when all validation is applied and there are no errors', () => { + const configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + + vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(configDatafile); + + const configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + // stubJsonSchemaValidator.returns(true); + mockJsonSchemaValidator.mockReturnValueOnce(true); + + const result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + jsonSchemaValidator: mockJsonSchemaValidator, + logger: logger, + }); + + expect(result).toMatchObject(configObj); + }); + + it('should throw an error when validateDatafile throws', function() { + vi.spyOn(configValidator, 'validateDatafile').mockImplementationOnce(() => { + throw new Error(); + }); + mockJsonSchemaValidator.mockReturnValueOnce(true); + + expect(() => + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: mockJsonSchemaValidator, + logger: logger, + }) + ).toThrowError(); + }); + + it('should throw an error when jsonSchemaValidator.validate throws', function() { + vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(true); + mockJsonSchemaValidator.mockImplementationOnce(() => { + throw new Error(); + }); + + expect(() => + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: mockJsonSchemaValidator, + logger: logger, + }) + ).toThrowError(); + }); + + it('should skip json validation when jsonSchemaValidator is not provided', function() { + const configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + + vi.spyOn(configValidator, 'validateDatafile').mockReturnValueOnce(configDatafile); + + const configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + const result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + logger: logger, + }); + + expect(result).toMatchObject(configObj); + expect(logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js new file mode 100644 index 000000000..d69afda46 --- /dev/null +++ b/lib/project_config/project_config.tests.js @@ -0,0 +1,974 @@ +/** + * Copyright 2016-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import sinon from 'sinon'; +import { assert } from 'chai'; +import { forEach, cloneDeep } from 'lodash'; +import { sprintf } from '../utils/fns'; +import fns from '../utils/fns'; +import projectConfig from './project_config'; +import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; +import testDatafile from '../tests/test_data'; +import configValidator from '../utils/config_validator'; +import { + INVALID_EXPERIMENT_ID, + INVALID_EXPERIMENT_KEY, + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + UNRECOGNIZED_ATTRIBUTE, + VARIABLE_KEY_NOT_IN_DATAFILE, + FEATURE_NOT_IN_DATAFILE, + UNABLE_TO_CAST_VALUE +} from 'error_message'; + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) + +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); +var logger = createLogger(); + +describe('lib/core/project_config', function() { + describe('createProjectConfig method', function() { + it('should set properties correctly when createProjectConfig is called', function() { + var testData = testDatafile.getTestProjectConfig(); + var configObj = projectConfig.createProjectConfig(testData); + + forEach(testData.audiences, function(audience) { + audience.conditions = JSON.parse(audience.conditions); + }); + + assert.strictEqual(configObj.accountId, testData.accountId); + assert.strictEqual(configObj.projectId, testData.projectId); + assert.strictEqual(configObj.revision, testData.revision); + assert.deepEqual(configObj.events, testData.events); + assert.deepEqual(configObj.audiences, testData.audiences); + testData.groups.forEach(function(group) { + group.experiments.forEach(function(experiment) { + experiment.groupId = group.id; + experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + }); + }); + assert.deepEqual(configObj.groups, testData.groups); + + var expectedGroupIdMap = { + 666: testData.groups[0], + 667: testData.groups[1], + }; + + assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap); + + var expectedExperiments = testData.experiments; + forEach(configObj.groupIdMap, function(group, Id) { + forEach(group.experiments, function(experiment) { + experiment.groupId = Id; + expectedExperiments.push(experiment); + }); + }); + + forEach(expectedExperiments, function(experiment) { + experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); + }); + + assert.deepEqual(configObj.experiments, expectedExperiments); + + var expectedAttributeKeyMap = { + browser_type: testData.attributes[0], + boolean_key: testData.attributes[1], + integer_key: testData.attributes[2], + double_key: testData.attributes[3], + valid_positive_number: testData.attributes[4], + valid_negative_number: testData.attributes[5], + invalid_number: testData.attributes[6], + array: testData.attributes[7], + }; + + assert.deepEqual(configObj.attributeKeyMap, expectedAttributeKeyMap); + + var expectedExperimentKeyMap = { + testExperiment: configObj.experiments[0], + testExperimentWithAudiences: configObj.experiments[1], + testExperimentNotRunning: configObj.experiments[2], + testExperimentLaunched: configObj.experiments[3], + groupExperiment1: configObj.experiments[4], + groupExperiment2: configObj.experiments[5], + overlappingGroupExperiment1: configObj.experiments[6], + }; + + assert.deepEqual(configObj.experimentKeyMap, expectedExperimentKeyMap); + + var expectedEventKeyMap = { + testEvent: testData.events[0], + 'Total Revenue': testData.events[1], + testEventWithAudiences: testData.events[2], + testEventWithoutExperiments: testData.events[3], + testEventWithExperimentNotRunning: testData.events[4], + testEventWithMultipleExperiments: testData.events[5], + testEventLaunched: testData.events[6], + }; + + assert.deepEqual(configObj.eventKeyMap, expectedEventKeyMap); + + var expectedExperimentIdMap = { + '111127': configObj.experiments[0], + '122227': configObj.experiments[1], + '133337': configObj.experiments[2], + '144447': configObj.experiments[3], + '442': configObj.experiments[4], + '443': configObj.experiments[5], + '444': configObj.experiments[6], + }; + + assert.deepEqual(configObj.experimentIdMap, expectedExperimentIdMap); + + var expectedVariationKeyMap = {}; + expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[0].key] = + testData.experiments[0].variations[0]; + expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[1].key] = + testData.experiments[0].variations[1]; + expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[0].key] = + testData.experiments[1].variations[0]; + expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[1].key] = + testData.experiments[1].variations[1]; + expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[0].key] = + testData.experiments[2].variations[0]; + expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[1].key] = + testData.experiments[2].variations[1]; + expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[0].key] = + configObj.experiments[3].variations[0]; + expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[1].key] = + configObj.experiments[3].variations[1]; + expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[0].key] = + configObj.experiments[4].variations[0]; + expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[1].key] = + configObj.experiments[4].variations[1]; + expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[0].key] = + configObj.experiments[5].variations[0]; + expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[1].key] = + configObj.experiments[5].variations[1]; + expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[0].key] = + configObj.experiments[6].variations[0]; + expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[1].key] = + configObj.experiments[6].variations[1]; + + var expectedVariationIdMap = { + '111128': testData.experiments[0].variations[0], + '111129': testData.experiments[0].variations[1], + '122228': testData.experiments[1].variations[0], + '122229': testData.experiments[1].variations[1], + '133338': testData.experiments[2].variations[0], + '133339': testData.experiments[2].variations[1], + '144448': testData.experiments[3].variations[0], + '144449': testData.experiments[3].variations[1], + '551': configObj.experiments[4].variations[0], + '552': configObj.experiments[4].variations[1], + '661': configObj.experiments[5].variations[0], + '662': configObj.experiments[5].variations[1], + '553': configObj.experiments[6].variations[0], + '554': configObj.experiments[6].variations[1], + }; + }); + + it('should not mutate the datafile', function() { + var datafile = testDatafile.getTypedAudiencesConfig(); + var datafileClone = cloneDeep(datafile); + projectConfig.createProjectConfig(datafile); + assert.deepEqual(datafileClone, datafile); + }); + + describe('feature management', function() { + var configObj; + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + }); + + it('creates a rolloutIdMap from rollouts in the datafile', function() { + assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); + }); + + it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() { + assert.deepEqual( + configObj.variationVariableUsageMap, + testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap + ); + }); + + it('creates a featureKeyMap from feature flags in the datafile', function() { + assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); + }); + + it('adds variations from rollout experiments to variationIdMap', function() { + assert.deepEqual(configObj.variationIdMap['594032'], { + variables: [ + { value: 'true', id: '4919852825313280' }, + { value: '395', id: '5482802778734592' }, + { value: '4.99', id: '6045752732155904' }, + { value: 'Hello audience', id: '6327227708866560' }, + { value: '{ "count": 2, "message": "Hello audience" }', id: '8765345281230956' }, + ], + featureEnabled: true, + key: '594032', + id: '594032', + }); + assert.deepEqual(configObj.variationIdMap['594038'], { + variables: [ + { value: 'false', id: '4919852825313280' }, + { value: '400', id: '5482802778734592' }, + { value: '14.99', id: '6045752732155904' }, + { value: 'Hello', id: '6327227708866560' }, + { value: '{ "count": 1, "message": "Hello" }', id: '8765345281230956' }, + ], + featureEnabled: false, + key: '594038', + id: '594038', + }); + assert.deepEqual(configObj.variationIdMap['594061'], { + variables: [ + { value: '27.34', id: '5060590313668608' }, + { value: 'Winter is NOT coming', id: '5342065290379264' }, + { value: '10003', id: '6186490220511232' }, + { value: 'false', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594061', + id: '594061', + }); + assert.deepEqual(configObj.variationIdMap['594067'], { + variables: [ + { value: '30.34', id: '5060590313668608' }, + { value: 'Winter is coming definitely', id: '5342065290379264' }, + { value: '500', id: '6186490220511232' }, + { value: 'true', id: '6467965197221888' }, + ], + featureEnabled: true, + key: '594067', + id: '594067', + }); + }); + }); + + describe('flag variations', function() { + var configObj; + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); + }); + + it('it should populate flagVariationsMap correctly', function() { + var allVariationsForFlag = configObj.flagVariationsMap; + var feature1Variations = allVariationsForFlag.feature_1; + var feature2Variations = allVariationsForFlag.feature_2; + var feature3Variations = allVariationsForFlag.feature_3; + var feature1VariationsKeys = feature1Variations.map(variation => { + return variation.key; + }, {}); + var feature2VariationsKeys = feature2Variations.map(variation => { + return variation.key; + }, {}); + var feature3VariationsKeys = feature3Variations.map(variation => { + return variation.key; + }, {}); + + assert.deepEqual(feature1VariationsKeys, ['a', 'b', '3324490633', '3324490562', '18257766532']); + assert.deepEqual(feature2VariationsKeys, ['variation_with_traffic', 'variation_no_traffic']); + assert.deepEqual(feature3VariationsKeys, []); + }); + }); + }); + + describe('projectConfig helper methods', function() { + var testData = cloneDeep(testDatafile.getTestProjectConfig()); + var configObj; + var createdLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + + beforeEach(function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + sinon.stub(createdLogger, 'warn'); + }); + + afterEach(function() { + createdLogger.warn.restore(); + }); + + it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { + assert.strictEqual( + projectConfig.getExperimentId(configObj, testData.experiments[0].key), + testData.experiments[0].id + ); + }); + + it('should throw error for invalid experiment key in getExperimentId', function() { + const ex = assert.throws(function() { + projectConfig.getExperimentId(configObj, 'invalidExperimentKey'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); + assert.deepEqual(ex.params, ['invalidExperimentKey']); + }); + + it('should retrieve layer ID for valid experiment key in getLayerId', function() { + assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4'); + }); + + it('should throw error for invalid experiment key in getLayerId', function() { + const ex = assert.throws(function() { + projectConfig.getLayerId(configObj, 'invalidExperimentKey'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentKey']); + }); + + it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { + assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094'); + }); + + it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { + assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent'); + }); + + it('should return null for invalid attribute key in getAttributeId', function() { + assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)); + + assert.deepEqual(createdLogger.warn.lastCall.args, [UNRECOGNIZED_ATTRIBUTE, 'invalidAttributeKey']); + }); + + it('should return null for invalid attribute key in getAttributeId', function() { + // Adding attribute in key map with reserved prefix + configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { + id: '42', + key: '$opt_some_reserved_attribute', + }; + assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger), '42'); + + assert.deepEqual(createdLogger.warn.lastCall.args, [UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, '$opt_some_reserved_attribute', '$opt_']); + }); + + it('should retrieve event ID for valid event key in getEventId', function() { + assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095'); + }); + + it('should return null for invalid event key in getEventId', function() { + assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey')); + }); + + it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { + assert.strictEqual( + projectConfig.getExperimentStatus(configObj, testData.experiments[0].key), + testData.experiments[0].status + ); + }); + + it('should throw error for invalid experiment key in getExperimentStatus', function() { + const ex = assert.throws(function() { + projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_KEY); + assert.deepEqual(ex.params, ['invalidExperimentKey']); + }); + + it('should return true if experiment status is set to Running in isActive', function() { + assert.isTrue(projectConfig.isActive(configObj, 'testExperiment')); + }); + + it('should return false if experiment status is not set to Running in isActive', function() { + assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning')); + }); + + it('should return true if experiment status is set to Running in isRunning', function() { + assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment')); + }); + + it('should return false if experiment status is not set to Running in isRunning', function() { + assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched')); + }); + + it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { + assert.deepEqual( + projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id), + testData.experiments[0].variations[0].key + ); + }); + + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + assert.deepEqual( + projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), + testData.experiments[0].trafficAllocation + ); + }); + + it('should throw error for invalid experient key in getTrafficAllocation', function() { + const ex = assert.throws(function() { + projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentId']); + }); + + describe('#getVariationIdFromExperimentAndVariationKey', function() { + it('should return the variation id for the given experiment key and variation key', function() { + assert.strictEqual( + projectConfig.getVariationIdFromExperimentAndVariationKey( + configObj, + testData.experiments[0].key, + testData.experiments[0].variations[0].key + ), + testData.experiments[0].variations[0].id + ); + }); + }); + + describe('#getSendFlagDecisionsValue', function() { + it('should return false when sendFlagDecisions is undefined', function() { + configObj.sendFlagDecisions = undefined; + assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); + }); + + it('should return false when sendFlagDecisions is set to false', function() { + configObj.sendFlagDecisions = false; + assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), false); + }); + + it('should return true when sendFlagDecisions is set to true', function() { + configObj.sendFlagDecisions = true; + assert.deepEqual(projectConfig.getSendFlagDecisionsValue(configObj), true); + }); + }); + + describe('feature management', function() { + var featureManagementLogger = createLogger({ logLevel: LOG_LEVEL.INFO }); + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + sinon.stub(featureManagementLogger, 'warn'); + sinon.stub(featureManagementLogger, 'error'); + sinon.stub(featureManagementLogger, 'info'); + sinon.stub(featureManagementLogger, 'debug'); + }); + + afterEach(function() { + featureManagementLogger.warn.restore(); + featureManagementLogger.error.restore(); + featureManagementLogger.info.restore(); + featureManagementLogger.debug.restore(); + }); + + describe('getVariableForFeature', function() { + it('should return a variable object for a valid variable and feature key', function() { + var featureKey = 'test_feature_for_experiment'; + var variableKey = 'num_buttons'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.deepEqual(result, { + type: 'integer', + key: 'num_buttons', + id: '4792309476491264', + defaultValue: '10', + }); + }); + + it('should return null for an invalid variable key and a valid feature key', function() { + var featureKey = 'test_feature_for_experiment'; + var variableKey = 'notARealVariable____'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.strictEqual(result, null); + sinon.assert.calledOnce(featureManagementLogger.error); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [VARIABLE_KEY_NOT_IN_DATAFILE, 'notARealVariable____', 'test_feature_for_experiment']); + }); + + it('should return null for an invalid feature key', function() { + var featureKey = 'notARealFeature_____'; + var variableKey = 'num_buttons'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.strictEqual(result, null); + sinon.assert.calledOnce(featureManagementLogger.error); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); + }); + + it('should return null for an invalid variable key and an invalid feature key', function() { + var featureKey = 'notARealFeature_____'; + var variableKey = 'notARealVariable____'; + var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); + assert.strictEqual(result, null); + sinon.assert.calledOnce(featureManagementLogger.error); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [FEATURE_NOT_IN_DATAFILE, 'notARealFeature_____']); + }); + }); + + describe('getVariableValueForVariation', function() { + it('returns a value for a valid variation and variable', function() { + var variation = configObj.variationIdMap['594096']; + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, '2'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + assert.strictEqual(result, 'true'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + assert.strictEqual(result, 'Buy me NOW'); + + variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; + result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); + assert.strictEqual(result, '20.25'); + }); + + it('returns null for a null variation', function() { + var variation = null; + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null for a null variable', function() { + var variation = configObj.variationIdMap['594096']; + var variable = null; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null for a null variation and null variable', function() { + var variation = null; + var variable = null; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null for a variation whose id is not in the datafile', function() { + var variation = { + key: 'some_variation', + id: '999999999999', + variables: [], + }; + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.strictEqual(result, null); + }); + + it('returns null if the variation does not have a value for this variable', function() { + var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it + var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; + var result = projectConfig.getVariableValueForVariation( + configObj, + variable, + variation, + featureManagementLogger + ); + assert.isNull(result); + }); + }); + + describe('getTypeCastValue', function() { + it('can cast a boolean', function() { + var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); + assert.strictEqual(result, true); + result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); + assert.strictEqual(result, false); + }); + + it('can cast an integer', function() { + var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); + assert.strictEqual(result, 50); + var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); + assert.strictEqual(result, -7); + var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); + assert.strictEqual(result, 0); + }); + + it('can cast a double', function() { + var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); + assert.strictEqual(result, 89.99); + var result = projectConfig.getTypeCastValue( + '-257.21', + FEATURE_VARIABLE_TYPES.DOUBLE, + featureManagementLogger + ); + assert.strictEqual(result, -257.21); + var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); + assert.strictEqual(result, 0); + var result = projectConfig.getTypeCastValue('10', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); + assert.strictEqual(result, 10); + }); + + it('can return a string unmodified', function() { + var result = projectConfig.getTypeCastValue( + 'message', + FEATURE_VARIABLE_TYPES.STRING, + featureManagementLogger + ); + assert.strictEqual(result, 'message'); + }); + + it('returns null and logs an error for an invalid boolean', function() { + var result = projectConfig.getTypeCastValue( + 'notabool', + FEATURE_VARIABLE_TYPES.BOOLEAN, + featureManagementLogger + ); + assert.strictEqual(result, null); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notabool', 'boolean']); + }); + + it('returns null and logs an error for an invalid integer', function() { + var result = projectConfig.getTypeCastValue( + 'notanint', + FEATURE_VARIABLE_TYPES.INTEGER, + featureManagementLogger + ); + assert.strictEqual(result, null); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notanint', 'integer']); + }); + + it('returns null and logs an error for an invalid double', function() { + var result = projectConfig.getTypeCastValue( + 'notadouble', + FEATURE_VARIABLE_TYPES.DOUBLE, + featureManagementLogger + ); + assert.strictEqual(result, null); + + assert.deepEqual(featureManagementLogger.error.lastCall.args, [UNABLE_TO_CAST_VALUE, 'notadouble', 'double']); + }); + }); + }); + + describe('#getAudiencesById', function() { + beforeEach(function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + }); + + it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { + assert.deepEqual(projectConfig.getAudiencesById(configObj), testDatafile.typedAudiencesById); + }); + }); + + describe('#getExperimentAudienceConditions', function() { + it('should retrieve audiences for valid experiment key', function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id), [ + '11154', + ]); + }); + + it('should throw error for invalid experiment key', function() { + configObj = projectConfig.createProjectConfig(cloneDeep(testData)); + const ex = assert.throws(function() { + projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentId']); + }); + + it('should return experiment audienceIds if experiment has no audienceConditions', function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + var result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); + assert.deepEqual(result, [ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ]); + }); + + it('should return experiment audienceConditions if experiment has audienceConditions', function() { + configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); + // audience_combinations_experiment has both audienceConditions and audienceIds + // audienceConditions should be preferred over audienceIds + var result = projectConfig.getExperimentAudienceConditions(configObj, '1323241598'); + assert.deepEqual(result, [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ]); + }); + }); + + describe('#isFeatureExperiment', function() { + it('returns true for a feature test', function() { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); + var result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' + assert.isTrue(result); + }); + + it('returns false for an A/B test', function() { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + assert.isFalse(result); + }); + + it('returns true for a feature test in a mutex group', function() { + var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + assert.isTrue(result); + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + assert.isTrue(result); + }); + }); + + describe('#getAudienceSegments', function() { + it('returns all qualified segments from an audience', function() { + const dummyQualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); + assert.deepEqual(dummyQualifiedAudienceJsonSegments, ['odp-segment-1']); + + const dummyUnqualifiedAudienceJson = { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'invalid', + }, + ], + ], + ], + name: 'odp-segment-1', + }; + + const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); + assert.deepEqual(dummyUnqualifiedAudienceJsonSegments, []); + }); + + it('returns false for an A/B test', function() { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + assert.isFalse(result); + }); + + it('returns true for a feature test in a mutex group', function() { + var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + assert.isTrue(result); + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + assert.isTrue(result); + }); + }); + }); + + describe('integrations', () => { + describe('#withSegments', () => { + var config; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.exists(config.integrations); + assert.equal(config.integrations.length, 4); + }); + + it('should populate odpIntegrationConfig', () => { + assert.isTrue(config.odpIntegrationConfig.integrated); + assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); + assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); + assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); + assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']); + }); + }); + + describe('#withoutSegments', () => { + var config; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.exists(config.integrations); + assert.equal(config.integrations.length, 3); + }); + + it('should populate odpIntegrationConfig', () => { + assert.isTrue(config.odpIntegrationConfig.integrated); + assert.equal(config.odpIntegrationConfig.odpConfig.apiKey, 'W4WzcEs-ABgXorzY7h1LCQ'); + assert.equal(config.odpIntegrationConfig.odpConfig.apiHost, 'https://api.zaius.com'); + assert.equal(config.odpIntegrationConfig.odpConfig.pixelUrl, 'https://jumbe.zaius.com'); + assert.deepEqual(config.odpIntegrationConfig.odpConfig.segmentsToCheck, []); + }); + }); + + describe('#withoutValidIntegrationKey', () => { + it('should throw an error when parsing the project config due to integrations not containing a key', () => { + const odpIntegratedConfigWithoutKey = testDatafile.getOdpIntegratedConfigWithoutKey(); + assert.throws(() => { + projectConfig.createProjectConfig(odpIntegratedConfigWithoutKey); + }); + }); + }); + + describe('#withoutIntegrations', () => { + var config; + beforeEach(() => { + const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments(); + const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] }; + config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.equal(config.integrations.length, 0); + }); + + it('should populate odpIntegrationConfig', () => { + assert.isFalse(config.odpIntegrationConfig.integrated); + assert.isUndefined(config.odpIntegrationConfig.odpConfig); + }); + }); + }); +}); + +describe('#tryCreatingProjectConfig', function() { + var stubJsonSchemaValidator; + beforeEach(function() { + stubJsonSchemaValidator = sinon.stub().returns(true); + sinon.stub(configValidator, 'validateDatafile').returns(true); + sinon.spy(logger, 'error'); + }); + + afterEach(function() { + configValidator.validateDatafile.restore(); + logger.error.restore(); + }); + + it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function() { + var configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + configValidator.validateDatafile.returns(configDatafile); + var configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + stubJsonSchemaValidator.returns(true); + + var result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + + assert.deepInclude(result, configObj); + }); + + it('throws an error when validateDatafile throws', function() { + configValidator.validateDatafile.throws(); + stubJsonSchemaValidator.returns(true); + assert.throws(() => { + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + }); + }); + + it('throws an error when jsonSchemaValidator.validate throws', function() { + configValidator.validateDatafile.returns(true); + stubJsonSchemaValidator.throws(); + assert.throws(() => { + projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + }); + }); + + it('skips json validation when jsonSchemaValidator is not provided', function() { + var configDatafile = { + foo: 'bar', + experiments: [{ key: 'a' }, { key: 'b' }], + }; + + configValidator.validateDatafile.returns(configDatafile); + + var configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + var result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + logger: logger, + }); + + assert.deepInclude(result, configObj); + sinon.assert.notCalled(logger.error); + }); +}); diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts new file mode 100644 index 000000000..9a611ff1a --- /dev/null +++ b/lib/project_config/project_config.ts @@ -0,0 +1,983 @@ +/** + * Copyright 2016-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { find, objectEntries, objectValues, keyBy, assignBy } from '../utils/fns'; + +import { FEATURE_VARIABLE_TYPES } from '../utils/enums'; +import configValidator from '../utils/config_validator'; + +import { LoggerFacade } from '../logging/logger'; + +import { + Audience, + Experiment, + FeatureFlag, + FeatureVariable, + Group, + OptimizelyVariation, + Rollout, + TrafficAllocation, + Variation, + VariableType, + VariationVariable, + Integration, + FeatureVariableValue, + Holdout, +} from '../shared_types'; +import { OdpConfig, OdpIntegrationConfig } from '../odp/odp_config'; +import { Transformer } from '../utils/type'; +import { + EXPERIMENT_KEY_NOT_IN_DATAFILE, + FEATURE_NOT_IN_DATAFILE, + INVALID_EXPERIMENT_ID, + INVALID_EXPERIMENT_KEY, + MISSING_INTEGRATION_KEY, + UNABLE_TO_CAST_VALUE, + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + UNRECOGNIZED_ATTRIBUTE, + VARIABLE_KEY_NOT_IN_DATAFILE, + VARIATION_ID_NOT_IN_DATAFILE, +} from 'error_message'; +import { SKIPPING_JSON_VALIDATION, VALID_DATAFILE } from 'log_message'; +import { OptimizelyError } from '../error/optimizly_error'; +import * as featureToggle from '../feature_toggle'; + +interface TryCreatingProjectConfigConfig { + // TODO[OASIS-6649]: Don't use object type + // eslint-disable-next-line @typescript-eslint/ban-types + datafile: string | object; + jsonSchemaValidator?: Transformer<unknown, boolean>; + logger?: LoggerFacade; +} + +interface Event { + key: string; + id: string; + experimentIds: string[]; +} + +interface VariableUsageMap { + [id: string]: VariationVariable; +} + +export type Region = 'US' | 'EU'; + +export interface ProjectConfig { + region: Region; + revision: string; + projectId: string; + sdkKey: string; + environmentKey: string; + sendFlagDecisions?: boolean; + experimentKeyMap: { [key: string]: Experiment }; + featureKeyMap: { + [key: string]: FeatureFlag; + }; + rollouts: Rollout[]; + featureFlags: FeatureFlag[]; + experimentIdMap: { [id: string]: Experiment }; + experimentFeatureMap: { [key: string]: string[] }; + experiments: Experiment[]; + eventKeyMap: { [key: string]: Event }; + audiences: Audience[]; + attributeKeyMap: { [key: string]: { id: string } }; + attributeIdMap: { [id: string]: { key: string } }; + variationIdMap: { [id: string]: OptimizelyVariation }; + variationVariableUsageMap: { [id: string]: VariableUsageMap }; + audiencesById: { [id: string]: Audience }; + __datafileStr: string; + groupIdMap: { [id: string]: Group }; + groups: Group[]; + events: Event[]; + attributes: Array<{ id: string; key: string }>; + typedAudiences: Audience[]; + rolloutIdMap: { [id: string]: Rollout }; + anonymizeIP?: boolean | null; + botFiltering?: boolean; + accountId: string; + flagRulesMap: { [key: string]: Experiment[] }; + flagVariationsMap: { [key: string]: Variation[] }; + integrations: Integration[]; + integrationKeyMap?: { [key: string]: Integration }; + odpIntegrationConfig: OdpIntegrationConfig; + holdouts: Holdout[]; + holdoutIdMap?: { [id: string]: Holdout }; + globalHoldouts: Holdout[]; + includedHoldouts: { [key: string]: Holdout[]; } + excludedHoldouts: { [key: string]: Holdout[]; } + flagHoldoutsMap: { [key: string]: Holdout[]; } +} + +const EXPERIMENT_RUNNING_STATUS = 'Running'; +const RESERVED_ATTRIBUTE_PREFIX = '$opt_'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { + const datafileCopy = { ...datafile }; + + datafileCopy.audiences = (datafile.audiences || []).map((audience: Audience) => { + return { ...audience }; + }); + datafileCopy.experiments = (datafile.experiments || []).map((experiment: Experiment) => { + return { ...experiment }; + }); + datafileCopy.featureFlags = (datafile.featureFlags || []).map((featureFlag: FeatureFlag) => { + return { ...featureFlag }; + }); + datafileCopy.groups = (datafile.groups || []).map((group: Group) => { + const groupCopy = { ...group }; + groupCopy.experiments = (group.experiments || []).map(experiment => { + return { ...experiment }; + }); + return groupCopy; + }); + datafileCopy.rollouts = (datafile.rollouts || []).map((rollout: Rollout) => { + const rolloutCopy = { ...rollout }; + rolloutCopy.experiments = (rollout.experiments || []).map(experiment => { + return { ...experiment }; + }); + return rolloutCopy; + }); + + datafileCopy.environmentKey = datafile.environmentKey ?? ''; + datafileCopy.sdkKey = datafile.sdkKey ?? ''; + + return datafileCopy; +} + +/** + * Creates projectConfig object to be used for quick project property lookup + * @param {Object} datafileObj JSON datafile representing the project + * @param {string|null} datafileStr JSON string representation of the datafile + * @return {ProjectConfig} Object representing project configuration + */ +export const createProjectConfig = function(datafileObj?: JSON, datafileStr: string | null = null): ProjectConfig { + const projectConfig = createMutationSafeDatafileCopy(datafileObj); + + if (!projectConfig.region) { + projectConfig.region = 'US'; // Default to US region if not specified + } + + projectConfig.__datafileStr = datafileStr === null ? JSON.stringify(datafileObj) : datafileStr; + + /* + * Conditions of audiences in projectConfig.typedAudiences are not + * expected to be string-encoded as they are here in projectConfig.audiences. + */ + (projectConfig.audiences || []).forEach(audience => { + audience.conditions = JSON.parse(audience.conditions as string); + }); + + + projectConfig.audiencesById = {}; + assignBy(projectConfig.audiences, 'id', projectConfig.audiencesById); + assignBy(projectConfig.typedAudiences, 'id', projectConfig.audiencesById); + + projectConfig.attributes = projectConfig.attributes || []; + projectConfig.attributeKeyMap = {}; + projectConfig.attributeIdMap = {}; + projectConfig.attributes.forEach(attribute => { + projectConfig.attributeKeyMap[attribute.key] = attribute; + projectConfig.attributeIdMap[attribute.id] = attribute; + }); + + projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key'); + projectConfig.groupIdMap = keyBy(projectConfig.groups, 'id'); + + let experiments; + Object.keys(projectConfig.groupIdMap || {}).forEach(Id => { + experiments = projectConfig.groupIdMap[Id].experiments; + (experiments || []).forEach(experiment => { + experiment.groupId = Id; + projectConfig.experiments.push(experiment); + }); + }); + + projectConfig.rolloutIdMap = keyBy(projectConfig.rollouts || [], 'id'); + objectValues(projectConfig.rolloutIdMap || {}).forEach(rollout => { + (rollout.experiments || []).forEach(experiment => { + experiment.isRollout = true + projectConfig.experiments.push(experiment); + // Creates { <variationKey>: <variation> } map inside of the experiment + experiment.variationKeyMap = keyBy(experiment.variations, 'key'); + }); + }); + + const allSegmentsSet = new Set<string>(); + + Object.keys(projectConfig.audiencesById) + .map(audience => getAudienceSegments(projectConfig.audiencesById[audience])) + .forEach(audienceSegments => { + audienceSegments.forEach(segment => { + allSegmentsSet.add(segment); + }); + }); + + const allSegments = Array.from(allSegmentsSet); + + let odpIntegrated = false; + let odpApiHost = ''; + let odpApiKey = ''; + let odpPixelUrl = ''; + + if (projectConfig.integrations) { + projectConfig.integrationKeyMap = keyBy(projectConfig.integrations, 'key'); + + projectConfig.integrations.forEach(integration => { + if (!('key' in integration)) { + throw new OptimizelyError(MISSING_INTEGRATION_KEY); + } + + if (integration.key === 'odp') { + odpIntegrated = true; + odpApiKey = odpApiKey || integration.publicKey || ''; + odpApiHost = odpApiHost || integration.host || ''; + odpPixelUrl = odpPixelUrl || integration.pixelUrl || ''; + } + }); + } + + if (odpIntegrated) { + projectConfig.odpIntegrationConfig = { + integrated: true, + odpConfig: new OdpConfig(odpApiKey, odpApiHost, odpPixelUrl, allSegments), + } + } else { + projectConfig.odpIntegrationConfig = { integrated: false }; + } + + projectConfig.experimentKeyMap = keyBy(projectConfig.experiments, 'key'); + projectConfig.experimentIdMap = keyBy(projectConfig.experiments, 'id'); + + projectConfig.variationIdMap = {}; + projectConfig.variationVariableUsageMap = {}; + (projectConfig.experiments || []).forEach(experiment => { + // Creates { <variationKey>: <variation> } map inside of the experiment + experiment.variationKeyMap = keyBy(experiment.variations, 'key'); + + assignBy(experiment.variations, 'id', projectConfig.variationIdMap); + + objectValues(experiment.variationKeyMap || {}).forEach(variation => { + if (variation.variables) { + projectConfig.variationVariableUsageMap[variation.id] = keyBy(variation.variables, 'id'); + } + }); + }); + + // Object containing experiment Ids that exist in any feature + // for checking that experiment is a feature experiment or not. + projectConfig.experimentFeatureMap = {}; + + projectConfig.featureKeyMap = keyBy(projectConfig.featureFlags || [], 'key'); + objectValues(projectConfig.featureKeyMap || {}).forEach(feature => { + // Json type is represented in datafile as a subtype of string for the sake of backwards compatibility. + // Converting it to a first-class json type while creating Project Config + feature.variables.forEach(variable => { + if (variable.type === FEATURE_VARIABLE_TYPES.STRING && variable.subType === FEATURE_VARIABLE_TYPES.JSON) { + variable.type = FEATURE_VARIABLE_TYPES.JSON as VariableType; + delete variable.subType; + } + }); + + feature.variableKeyMap = keyBy(feature.variables, 'key'); + (feature.experimentIds || []).forEach(experimentId => { + // Add this experiment in experiment-feature map. + if (projectConfig.experimentFeatureMap[experimentId]) { + projectConfig.experimentFeatureMap[experimentId].push(feature.id); + } else { + projectConfig.experimentFeatureMap[experimentId] = [feature.id]; + } + }); + }); + + // all rules (experiment rules and delivery rules) for each flag + projectConfig.flagRulesMap = {}; + + (projectConfig.featureFlags || []).forEach(featureFlag => { + const flagRuleExperiments: Experiment[] = []; + featureFlag.experimentIds.forEach(experimentId => { + const experiment = projectConfig.experimentIdMap[experimentId]; + if (experiment) { + flagRuleExperiments.push(experiment); + } + }); + + const rollout = projectConfig.rolloutIdMap[featureFlag.rolloutId]; + if (rollout) { + flagRuleExperiments.push(...rollout.experiments); + } + + projectConfig.flagRulesMap[featureFlag.key] = flagRuleExperiments; + }); + + // all variations for each flag + // - datafile does not contain a separate entity for this. + // - we collect variations used in each rule (experiment rules and delivery rules) + projectConfig.flagVariationsMap = {}; + + objectEntries(projectConfig.flagRulesMap || {}).forEach(([flagKey, rules]) => { + const variations: OptimizelyVariation[] = []; + rules.forEach(rule => { + rule.variations.forEach(variation => { + if (!find(variations, item => item.id === variation.id)) { + variations.push(variation); + } + }); + }); + projectConfig.flagVariationsMap[flagKey] = variations; + }); + + parseHoldoutsConfig(projectConfig); + + return projectConfig; +}; + +const parseHoldoutsConfig = (projectConfig: ProjectConfig): void => { + if (!featureToggle.holdout()) { + return; + } + + projectConfig.holdouts = projectConfig.holdouts || []; + projectConfig.holdoutIdMap = keyBy(projectConfig.holdouts, 'id'); + projectConfig.globalHoldouts = []; + projectConfig.includedHoldouts = {}; + projectConfig.excludedHoldouts = {}; + projectConfig.flagHoldoutsMap = {}; + + const featureFlagIdMap = keyBy(projectConfig.featureFlags, 'id'); + + projectConfig.holdouts.forEach((holdout) => { + if (!holdout.includedFlags) { + holdout.includedFlags = []; + } + + if (!holdout.excludedFlags) { + holdout.excludedFlags = []; + } + + holdout.variationKeyMap = keyBy(holdout.variations, 'key'); + + assignBy(holdout.variations, 'id', projectConfig.variationIdMap); + + if (holdout.includedFlags.length === 0) { + projectConfig.globalHoldouts.push(holdout); + + holdout.excludedFlags.forEach((flagId: string) => { + const flag = featureFlagIdMap[flagId]; + if (flag) { + const flagKey = flag.key; + if (!projectConfig.excludedHoldouts[flagKey]) { + projectConfig.excludedHoldouts[flagKey] = []; + } + projectConfig.excludedHoldouts[flagKey].push(holdout); + } + }); + } else { + holdout.includedFlags.forEach((flagId: string) => { + const flag = featureFlagIdMap[flagId]; + if (flag) { + const flagKey = flag.key; + if (!projectConfig.includedHoldouts[flagKey]) { + projectConfig.includedHoldouts[flagKey] = []; + } + projectConfig.includedHoldouts[flagKey].push(holdout); + } + }) + } + }); +} + +export const getHoldoutsForFlag = (projectConfig: ProjectConfig, flagKey: string): Holdout[] => { + if (projectConfig.flagHoldoutsMap[flagKey]) { + return projectConfig.flagHoldoutsMap[flagKey]; + } + + const flagHoldouts: Holdout[] = [ + ...projectConfig.globalHoldouts.filter((holdout) => { + return !(projectConfig.excludedHoldouts[flagKey] || []).includes(holdout); + }), + ...(projectConfig.includedHoldouts[flagKey] || []), + ]; + + projectConfig.flagHoldoutsMap[flagKey] = flagHoldouts; + return flagHoldouts; +} + +/** + * Extract all audience segments used in this audience's conditions + * @param {Audience} audience Object representing the audience being parsed + * @return {string[]} List of all audience segments + */ +export const getAudienceSegments = function(audience: Audience): string[] { + if (!audience.conditions) return []; + return getSegmentsFromConditions(audience.conditions); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getSegmentsFromConditions = (condition: any): string[] => { + const segments = []; + + if (isLogicalOperator(condition)) { + return []; + } else if (Array.isArray(condition)) { + condition.forEach(nextCondition => segments.push(...getSegmentsFromConditions(nextCondition))); + } else if (condition['match'] === 'qualified') { + segments.push(condition['value']); + } + + return segments; +}; + +function isLogicalOperator(condition: string): boolean { + return ['and', 'or', 'not'].includes(condition); +} + +/** + * Get experiment ID for the provided experiment key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentKey Experiment key for which ID is to be determined + * @return {string} Experiment ID corresponding to the provided experiment key + * @throws If experiment key is not in datafile + */ +export const getExperimentId = function(projectConfig: ProjectConfig, experimentKey: string): string { + const experiment = projectConfig.experimentKeyMap[experimentKey]; + if (!experiment) { + throw new OptimizelyError(INVALID_EXPERIMENT_KEY, experimentKey); + } + return experiment.id; +}; + +/** + * Get layer ID for the provided experiment key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentId Experiment ID for which layer ID is to be determined + * @return {string} Layer ID corresponding to the provided experiment key + * @throws If experiment key is not in datafile + */ +export const getLayerId = function(projectConfig: ProjectConfig, experimentId: string): string { + const experiment = projectConfig.experimentIdMap[experimentId]; + if (!experiment) { + throw new OptimizelyError(INVALID_EXPERIMENT_ID, experimentId); + } + return experiment.layerId; +}; + +/** + * Get attribute ID for the provided attribute key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} attributeKey Attribute key for which ID is to be determined + * @param {LogHandler} logger + * @return {string|null} Attribute ID corresponding to the provided attribute key. Attribute key if it is a reserved attribute. + */ +export const getAttributeId = function( + projectConfig: ProjectConfig, + attributeKey: string, + logger?: LoggerFacade +): string | null { + const attribute = projectConfig.attributeKeyMap[attributeKey]; + const hasReservedPrefix = attributeKey.indexOf(RESERVED_ATTRIBUTE_PREFIX) === 0; + if (attribute) { + if (hasReservedPrefix) { + logger?.warn( + UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX, + attributeKey, + RESERVED_ATTRIBUTE_PREFIX + ); + } + return attribute.id; + } else if (hasReservedPrefix) { + return attributeKey; + } + + logger?.warn(UNRECOGNIZED_ATTRIBUTE, attributeKey); + return null; +}; + +/** + * Get event ID for the provided + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} eventKey Event key for which ID is to be determined + * @return {string|null} Event ID corresponding to the provided event key + */ +export const getEventId = function(projectConfig: ProjectConfig, eventKey: string): string | null { + const event = projectConfig.eventKeyMap[eventKey]; + if (event) { + return event.id; + } + return null; +}; + +/** + * Get experiment status for the provided experiment key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentKey Experiment key for which status is to be determined + * @return {string} Experiment status corresponding to the provided experiment key + * @throws If experiment key is not in datafile + */ +export const getExperimentStatus = function(projectConfig: ProjectConfig, experimentKey: string): string { + const experiment = projectConfig.experimentKeyMap[experimentKey]; + if (!experiment) { + throw new OptimizelyError(INVALID_EXPERIMENT_KEY, experimentKey); + } + return experiment.status; +}; + +/** + * Returns whether experiment has a status of 'Running' + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentKey Experiment key for which status is to be compared with 'Running' + * @return {boolean} True if experiment status is set to 'Running', false otherwise + */ +export const isActive = function(projectConfig: ProjectConfig, experimentKey: string): boolean { + return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; +}; + +/** + * Determine for given experiment if event is running, which determines whether should be dispatched or not + * @param {ProjectConfig} configObj Object representing project configuration + * @param {string} experimentKey Experiment key for which the status is to be determined + * @return {boolean} True if the experiment is running + * False if the experiment is not running + * + */ +export const isRunning = function(projectConfig: ProjectConfig, experimentKey: string): boolean { + return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; +}; + +/** + * Get audience conditions for the experiment + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentId Experiment id for which audience conditions are to be determined + * @return {Array<string|string[]>} Audience conditions for the experiment - can be an array of audience IDs, or a + * nested array of conditions + * Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"] + * @throws If experiment key is not in datafile + */ +export const getExperimentAudienceConditions = function( + projectConfig: ProjectConfig, + experimentId: string +): Array<string | string[]> { + const experiment = projectConfig.experimentIdMap[experimentId]; + if (!experiment) { + throw new OptimizelyError(INVALID_EXPERIMENT_ID, experimentId); + } + + return experiment.audienceConditions || experiment.audienceIds; +}; + +/** + * Get variation key given experiment key and variation ID + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} variationId ID of the variation + * @return {string|null} Variation key or null if the variation ID is not found + */ +export const getVariationKeyFromId = function(projectConfig: ProjectConfig, variationId: string): string | null { + if (projectConfig.variationIdMap.hasOwnProperty(variationId)) { + return projectConfig.variationIdMap[variationId].key; + } + + return null; +}; + +/** + * Get variation given variation ID + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} variationId ID of the variation + * @return {Variation|null} Variation or null if the variation ID is not found + */ +export const getVariationFromId = function(projectConfig: ProjectConfig, variationId: string): Variation | null { + if (projectConfig.variationIdMap.hasOwnProperty(variationId)) { + return projectConfig.variationIdMap[variationId]; + } + + return null; +}; + +/** + * Get the variation ID given the experiment key and variation key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentKey Key of the experiment the variation belongs to + * @param {string} variationKey The variation key + * @return {string|null} Variation ID or null + */ +export const getVariationIdFromExperimentAndVariationKey = function( + projectConfig: ProjectConfig, + experimentKey: string, + variationKey: string +): string | null { + const experiment = projectConfig.experimentKeyMap[experimentKey]; + if (experiment.variationKeyMap.hasOwnProperty(variationKey)) { + return experiment.variationKeyMap[variationKey].id; + } + + return null; +}; + +/** + * Get experiment from provided experiment key + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentKey Event key for which experiment IDs are to be retrieved + * @return {Experiment} Experiment + * @throws If experiment key is not in datafile + */ +export const getExperimentFromKey = function(projectConfig: ProjectConfig, experimentKey: string): Experiment { + if (projectConfig.experimentKeyMap.hasOwnProperty(experimentKey)) { + const experiment = projectConfig.experimentKeyMap[experimentKey]; + if (experiment) { + return experiment; + } + } + + throw new OptimizelyError(EXPERIMENT_KEY_NOT_IN_DATAFILE, experimentKey); +}; + + +/** + * Given an experiment id, returns the traffic allocation within that experiment + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentId Id representing the experiment + * @return {TrafficAllocation[]} Traffic allocation for the experiment + * @throws If experiment key is not in datafile + */ +export const getTrafficAllocation = function(projectConfig: ProjectConfig, experimentId: string): TrafficAllocation[] { + const experiment = projectConfig.experimentIdMap[experimentId]; + if (!experiment) { + throw new OptimizelyError(INVALID_EXPERIMENT_ID, experimentId); + } + return experiment.trafficAllocation; +}; + +/** + * Get experiment from provided experiment id. Log an error if no experiment + * exists in the project config with the given ID. + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentId ID of desired experiment object + * @param {LogHandler} logger + * @return {Experiment|null} Experiment object or null + */ +export const getExperimentFromId = function( + projectConfig: ProjectConfig, + experimentId: string, + logger?: LoggerFacade +): Experiment | null { + if (projectConfig.experimentIdMap.hasOwnProperty(experimentId)) { + const experiment = projectConfig.experimentIdMap[experimentId]; + if (experiment) { + return experiment; + } + } + + logger?.error(INVALID_EXPERIMENT_ID, experimentId); + return null; +}; + +/** + * Returns flag variation for specified flagKey and variationKey + * @param {flagKey} string + * @param {variationKey} string + * @return {Variation|null} + */ +export const getFlagVariationByKey = function( + projectConfig: ProjectConfig, + flagKey: string, + variationKey: string +): Variation | null { + if (!projectConfig) { + return null; + } + + const variations = projectConfig.flagVariationsMap[flagKey]; + const result = find(variations, item => item.key === variationKey); + if (result) { + return result; + } + + return null; +}; + +/** + * Get feature from provided feature key. Log an error if no feature exists in + * the project config with the given key. + * @param {ProjectConfig} projectConfig + * @param {string} featureKey + * @param {LogHandler} logger + * @return {FeatureFlag|null} Feature object, or null if no feature with the given + * key exists + */ +export const getFeatureFromKey = function( + projectConfig: ProjectConfig, + featureKey: string, + logger?: LoggerFacade +): FeatureFlag | null { + if (projectConfig.featureKeyMap.hasOwnProperty(featureKey)) { + const feature = projectConfig.featureKeyMap[featureKey]; + if (feature) { + return feature; + } + } + + logger?.error(FEATURE_NOT_IN_DATAFILE, featureKey); + return null; +}; + +/** + * Get the variable with the given key associated with the feature with the + * given key. If the feature key or the variable key are invalid, log an error + * message. + * @param {ProjectConfig} projectConfig + * @param {string} featureKey + * @param {string} variableKey + * @param {LogHandler} logger + * @return {FeatureVariable|null} Variable object, or null one or both of the given + * feature and variable keys are invalid + */ +export const getVariableForFeature = function( + projectConfig: ProjectConfig, + featureKey: string, + variableKey: string, + logger?: LoggerFacade +): FeatureVariable | null { + const feature = projectConfig.featureKeyMap[featureKey]; + if (!feature) { + logger?.error(FEATURE_NOT_IN_DATAFILE, featureKey); + return null; + } + + const variable = feature.variableKeyMap[variableKey]; + if (!variable) { + logger?.error(VARIABLE_KEY_NOT_IN_DATAFILE, variableKey, featureKey); + return null; + } + + return variable; +}; + +/** + * Get the value of the given variable for the given variation. If the given + * variable has no value for the given variation, return null. Log an error message if the variation is invalid. If the + * variable or variation are invalid, return null. + * @param {ProjectConfig} projectConfig + * @param {FeatureVariable} variable + * @param {Variation} variation + * @param {LogHandler} logger + * @return {string|null} The value of the given variable for the given + * variation, or null if the given variable has no value + * for the given variation or if the variation or variable are invalid + */ +export const getVariableValueForVariation = function( + projectConfig: ProjectConfig, + variable: FeatureVariable, + variation: Variation, + logger?: LoggerFacade +): string | null { + if (!variable || !variation) { + return null; + } + + if (!projectConfig.variationVariableUsageMap.hasOwnProperty(variation.id)) { + logger?.error(VARIATION_ID_NOT_IN_DATAFILE, variation.id); + return null; + } + + const variableUsages = projectConfig.variationVariableUsageMap[variation.id]; + const variableUsage = variableUsages[variable.id]; + + return variableUsage ? variableUsage.value : null; +}; + +/** + * Given a variable value in string form, try to cast it to the argument type. + * If the type cast succeeds, return the type casted value, otherwise log an + * error and return null. + * @param {string} variableValue Variable value in string form + * @param {string} variableType Type of the variable whose value was passed + * in the first argument. Must be one of + * FEATURE_VARIABLE_TYPES in + * lib/utils/enums/index.js. The return value's + * type is determined by this argument (boolean + * for BOOLEAN, number for INTEGER or DOUBLE, + * and string for STRING). + * @param {LogHandler} logger Logger instance + * @returns {*} Variable value of the appropriate type, or + * null if the type cast failed + */ +export const getTypeCastValue = function( + variableValue: string, + variableType: VariableType, + logger?: LoggerFacade +): FeatureVariableValue { + let castValue : FeatureVariableValue; + + switch (variableType) { + case FEATURE_VARIABLE_TYPES.BOOLEAN: + if (variableValue !== 'true' && variableValue !== 'false') { + logger?.error(UNABLE_TO_CAST_VALUE, variableValue, variableType); + castValue = null; + } else { + castValue = variableValue === 'true'; + } + break; + + case FEATURE_VARIABLE_TYPES.INTEGER: + castValue = parseInt(variableValue, 10); + if (isNaN(castValue)) { + logger?.error(UNABLE_TO_CAST_VALUE, variableValue, variableType); + castValue = null; + } + break; + + case FEATURE_VARIABLE_TYPES.DOUBLE: + castValue = parseFloat(variableValue); + if (isNaN(castValue)) { + logger?.error(UNABLE_TO_CAST_VALUE, variableValue, variableType); + castValue = null; + } + break; + + case FEATURE_VARIABLE_TYPES.JSON: + try { + castValue = JSON.parse(variableValue); + } catch (e) { + logger?.error(UNABLE_TO_CAST_VALUE, variableValue, variableType); + castValue = null; + } + break; + + default: + // type is STRING + castValue = variableValue; + break; + } + + return castValue; +}; + +/** + * Returns an object containing all audiences in the project config. Keys are audience IDs + * and values are audience objects. + * @param {ProjectConfig} projectConfig + * @returns {{ [id: string]: Audience }} + */ +export const getAudiencesById = function(projectConfig: ProjectConfig): { [id: string]: Audience } { + return projectConfig.audiencesById; +}; + +/** + * Returns true if an event with the given key exists in the datafile, and false otherwise + * @param {ProjectConfig} projectConfig + * @param {string} eventKey + * @returns {boolean} + */ +export const eventWithKeyExists = function(projectConfig: ProjectConfig, eventKey: string): boolean { + return projectConfig.eventKeyMap.hasOwnProperty(eventKey); +}; + +/** + * Returns true if experiment belongs to any feature, false otherwise. + * @param {ProjectConfig} projectConfig + * @param {string} experimentId + * @returns {boolean} + */ +export const isFeatureExperiment = function(projectConfig: ProjectConfig, experimentId: string): boolean { + return projectConfig.experimentFeatureMap.hasOwnProperty(experimentId); +}; + +/** + * Returns the JSON string representation of the datafile + * @param {ProjectConfig} projectConfig + * @returns {string} + */ +export const toDatafile = function(projectConfig: ProjectConfig): string { + return projectConfig.__datafileStr; +}; + +/** + * @typedef {Object} + * @property {Object|null} configObj + * @property {Error|null} error + */ + +/** + * Try to create a project config object from the given datafile and + * configuration properties. + * Returns a ProjectConfig if successful. + * Otherwise, throws an error. + * @param {Object} config + * @param {Object|string} config.datafile + * @param {Object} config.jsonSchemaValidator + * @param {Object} config.logger + * @returns {Object} ProjectConfig + * @throws {Error} + */ +export const tryCreatingProjectConfig = function( + config: TryCreatingProjectConfigConfig +): ProjectConfig { + const newDatafileObj = configValidator.validateDatafile(config.datafile); + + if (config.jsonSchemaValidator) { + config.jsonSchemaValidator(newDatafileObj); + config.logger?.info(VALID_DATAFILE); + } else { + config.logger?.info(SKIPPING_JSON_VALIDATION); + } + + const createProjectConfigArgs = [newDatafileObj]; + if (typeof config.datafile === 'string') { + // Since config.datafile was validated above, we know that it is a valid JSON string + createProjectConfigArgs.push(config.datafile); + } + + const newConfigObj = createProjectConfig(...createProjectConfigArgs); + return newConfigObj; +}; + +/** + * Get the send flag decisions value + * @param {ProjectConfig} projectConfig + * @return {boolean} A boolean value that indicates if we should send flag decisions + */ +export const getSendFlagDecisionsValue = function(projectConfig: ProjectConfig): boolean { + return !!projectConfig.sendFlagDecisions; +}; + +export default { + createProjectConfig, + getExperimentId, + getLayerId, + getAttributeId, + getEventId, + getExperimentStatus, + isActive, + isRunning, + getExperimentAudienceConditions, + getVariationFromId, + getVariationKeyFromId, + getVariationIdFromExperimentAndVariationKey, + getExperimentFromKey, + getExperimentFromId, + getFlagVariationByKey, + getFeatureFromKey, + getVariableForFeature, + getVariableValueForVariation, + getTypeCastValue, + getSendFlagDecisionsValue, + getAudiencesById, + getAudienceSegments, + eventWithKeyExists, + isFeatureExperiment, + toDatafile, + tryCreatingProjectConfig, + getTrafficAllocation, +}; diff --git a/lib/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts new file mode 100644 index 000000000..76c9af736 --- /dev/null +++ b/lib/project_config/project_config_manager.spec.ts @@ -0,0 +1,569 @@ +/** + * Copyright 2019-2020, 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, vi } from 'vitest'; +import { LOGGER_NAME, ProjectConfigManagerImpl } from './project_config_manager'; +import { getMockLogger } from '../tests/mock/mock_logger'; +import { ServiceState } from '../service'; +import * as testData from '../tests/test_data'; +import { createProjectConfig } from './project_config'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { getMockDatafileManager } from '../tests/mock/mock_datafile_manager'; +import { wait } from '../tests/testUtils'; + +const cloneDeep = (x: any) => JSON.parse(JSON.stringify(x)); + +describe('ProjectConfigManagerImpl', () => { + describe('a logger is passed in the constructor', () => { + it('should set name on the logger passed into the constructor', () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger }); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME) + }); + + it('should pass a child logger to the datafileManager', () => { + const logger = getMockLogger(); + const childLogger = getMockLogger(); + logger.child.mockReturnValue(childLogger); + const datafileManager = getMockDatafileManager({}); + const datafileManagerSetLogger = vi.spyOn(datafileManager, 'setLogger'); + + const manager = new ProjectConfigManagerImpl({ logger, datafileManager }); + expect(datafileManagerSetLogger).toHaveBeenCalledWith(childLogger); + }); + }); + + describe('setLogger method', () => { + it('should set name on the logger', () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({}); + manager.setLogger(logger); + expect(logger.setName).toHaveBeenCalledWith(LOGGER_NAME) + }); + + it('should pass a child logger to the datafileManager', () => { + const logger = getMockLogger(); + const childLogger = getMockLogger(); + logger.child.mockReturnValue(childLogger); + const datafileManager = getMockDatafileManager({}); + const datafileManagerSetLogger = vi.spyOn(datafileManager, 'setLogger'); + + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.setLogger(logger); + expect(datafileManagerSetLogger).toHaveBeenCalledWith(childLogger); + }); + }); + + it('should reject onRunning() and log error if neither datafile nor a datafileManager is passed into the constructor', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger}); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + + it('should set status to Failed if neither datafile nor a datafileManager is passed into the constructor', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger}); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow(); + expect(manager.getState()).toBe(ServiceState.Failed); + }); + + it('should reject onTerminated if neither datafile nor a datafileManager is passed into the constructor', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger}); + manager.start(); + await expect(manager.onTerminated()).rejects.toThrow(); + }); + + describe('when constructed with only a datafile', () => { + it('should reject onRunning() and log error if the datafile is invalid', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafile: {}}); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + + it('should set status to Failed if the datafile is invalid', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafile: {}}); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow(); + expect(manager.getState()).toBe(ServiceState.Failed); + }); + + it('should reject onTerminated if the datafile is invalid', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger}); + manager.start(); + await expect(manager.onTerminated()).rejects.toThrow(); + }); + + it('should fulfill onRunning() and set status to Running if the datafile is valid', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()}); + manager.start(); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.getState()).toBe(ServiceState.Running); + }); + + it('should call onUpdate listeners registered before start() with the project config', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()}); + const listener = vi.fn(); + manager.onUpdate(listener); + manager.start(); + + await manager.onRunning(); + + expect(listener).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + }); + + it('should return the correct config from getConfig() both before or after onRunning() resolves', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafile: testData.getTestProjectConfig()}); + manager.start(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + await manager.onRunning(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + }); + }); + + describe('when constructed with a datafileManager', () => { + describe('when datafile is also provided', () => { + describe('when datafile is valid', () => { + it('should resolve onRunning() before datafileManger.onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ + onRunning: resolvablePromise<void>().promise, // this will not be resolved + }); + vi.spyOn(datafileManager, 'onRunning'); + const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + manager.start(); + + expect(datafileManager.onRunning).toHaveBeenCalled(); + await expect(manager.onRunning()).resolves.not.toThrow(); + }); + + it('should resolve onRunning() even if datafileManger.onRunning() rejects', async () => { + const onRunning = Promise.reject(new Error("onRunning error")); + const datafileManager = getMockDatafileManager({ + onRunning, + }); + vi.spyOn(datafileManager, 'onRunning'); + const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + manager.start(); + + expect(datafileManager.onRunning).toHaveBeenCalled(); + await expect(manager.onRunning()).resolves.not.toThrow(); + }); + + it('should call the onUpdate handler before datafileManger.onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ + onRunning: resolvablePromise<void>().promise, // this will not be resolved + }); + + const listener = vi.fn(); + + const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + manager.onUpdate(listener); + manager.start(); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + }); + + it('should return the correct config from getConfig() both before or after onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ + onRunning: resolvablePromise<void>().promise, // this will not be resolved + }); + + const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + manager.start(); + + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + await manager.onRunning(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + }); + }); + + describe('when datafile is invalid', () => { + it('should reject onRunning() with the same error if datafileManager.onRunning() rejects', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.reject(new Error('test error')) }); + const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow('DatafileManager failed to start, reason: test error'); + }); + + it('should resolve onRunning() if datafileManager.onUpdate() is fired and should update config', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + manager.start(); + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + }); + + it('should resolve onRunning(), update config and call onUpdate listeners if datafileManager.onUpdate() is fired', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + manager.start(); + const listener = vi.fn(); + manager.onUpdate(listener); + + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + }); + + it('should return undefined from getConfig() before onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + manager.start(); + expect(manager.getConfig()).toBeUndefined(); + }); + + it('should return the correct config from getConfig() after onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); + manager.start(); + + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + }); + }); + }); + + describe('when datafile is not provided', () => { + it('should reject onRunning() if datafileManager.onRunning() rejects', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.reject(new Error('test error')) }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow('DatafileManager failed to start, reason: test error'); + }); + + it('should reject onRunning() and onTerminated if datafileManager emits an invalid datafile in the first onUpdate', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + datafileManager.pushUpdate('foo'); + await expect(manager.onRunning()).rejects.toThrow(); + await expect(manager.onTerminated()).rejects.toThrow(); + }); + + it('should resolve onRunning(), update config and call onUpdate listeners if datafileManager.onUpdate() is fired', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + const listener = vi.fn(); + manager.onUpdate(listener); + + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(listener).toHaveBeenCalledWith(createProjectConfig(testData.getTestProjectConfig())); + }); + + it('should return undefined from getConfig() before onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + expect(manager.getConfig()).toBeUndefined(); + }); + + it('should return the correct config from getConfig() after onRunning() resolves', async () => { + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve() }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await expect(manager.onRunning()).resolves.not.toThrow(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + }); + }); + + it('should update the config and call onUpdate handlers when datafileManager onUpdate is fired with valid datafile', async () => { + const datafileManager = getMockDatafileManager({}); + + const datafile = testData.getTestProjectConfig(); + const manager = new ProjectConfigManagerImpl({ datafile, datafileManager }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + await manager.onRunning(); + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(datafile)); + + const updatedDatafile = cloneDeep(datafile); + updatedDatafile['revision'] = '99'; + datafileManager.pushUpdate(updatedDatafile); + await Promise.resolve(); + + expect(manager.getConfig()).toEqual(createProjectConfig(updatedDatafile)); + expect(listener).toHaveBeenNthCalledWith(2, createProjectConfig(updatedDatafile)); + }); + + it('should not call onUpdate handlers and should log error when datafileManager onUpdate is fired with invalid datafile', async () => { + const datafileManager = getMockDatafileManager({}); + + const logger = getMockLogger(); + const datafile = testData.getTestProjectConfig(); + const manager = new ProjectConfigManagerImpl({ logger, datafile, datafileManager }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + await manager.onRunning(); + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + + expect(listener).toHaveBeenCalledWith(createProjectConfig(datafile)); + + const updatedDatafile = {}; + datafileManager.pushUpdate(updatedDatafile); + await Promise.resolve(); + + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(listener).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalled(); + }); + + it('should use the JSON schema validator to validate the datafile', async () => { + const datafileManager = getMockDatafileManager({}); + + const datafile = testData.getTestProjectConfig(); + const jsonSchemaValidator = vi.fn().mockReturnValue(true); + const manager = new ProjectConfigManagerImpl({ datafile, datafileManager, jsonSchemaValidator }); + manager.start(); + + await manager.onRunning(); + + const updatedDatafile = cloneDeep(datafile); + updatedDatafile['revision'] = '99'; + datafileManager.pushUpdate(updatedDatafile); + await Promise.resolve(); + + expect(jsonSchemaValidator).toHaveBeenCalledTimes(2); + expect(jsonSchemaValidator).toHaveBeenNthCalledWith(1, datafile); + expect(jsonSchemaValidator).toHaveBeenNthCalledWith(2, updatedDatafile); + }); + + it('should not call onUpdate handlers when datafileManager onUpdate is fired with the same datafile', async () => { + const datafileManager = getMockDatafileManager({}); + + const datafile = testData.getTestProjectConfig(); + const manager = new ProjectConfigManagerImpl({ datafile, datafileManager }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + await manager.onRunning(); + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(datafile)); + + datafileManager.pushUpdate(cloneDeep(datafile)); + await Promise.resolve(); + + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should remove onUpdate handlers when the returned fuction is called', async () => { + const datafile = testData.getTestProjectConfig(); + const datafileManager = getMockDatafileManager({}); + + const manager = new ProjectConfigManagerImpl({ datafile }); + + const listener = vi.fn(); + const dispose = manager.onUpdate(listener); + + manager.start(); + + await manager.onRunning(); + expect(listener).toHaveBeenNthCalledWith(1, createProjectConfig(datafile)); + + dispose(); + + datafileManager.pushUpdate(cloneDeep(testData.getTestProjectConfigWithFeatures())); + await Promise.resolve(); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should work with datafile specified as string', async () => { + const datafile = testData.getTestProjectConfig(); + + const manager = new ProjectConfigManagerImpl({ datafile: JSON.stringify(datafile) }); + + const listener = vi.fn(); + manager.onUpdate(listener); + + manager.start(); + + await manager.onRunning(); + expect(listener).toHaveBeenCalledWith(createProjectConfig(datafile)); + expect(manager.getConfig()).toEqual(createProjectConfig(datafile)); + }); + + it('should reject onRunning() and log error if the datafile string is an invalid json', async () => { + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafile: 'foo'}); + manager.start(); + await expect(manager.onRunning()).rejects.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + + it('should reject onRunning() and log error if the datafile version is not supported', async () => { + const logger = getMockLogger(); + const datafile = testData.getUnsupportedVersionConfig(); + const manager = new ProjectConfigManagerImpl({ logger, datafile }); + manager.start(); + + await expect(manager.onRunning()).rejects.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + + describe('stop()', () => { + it('should reject onRunning() if stop is called when the datafileManager state is New', async () => { + const datafileManager = getMockDatafileManager({}); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + + expect(manager.getState()).toBe(ServiceState.New); + manager.stop(); + await expect(manager.onRunning()).rejects.toThrow(); + }); + + it('should reject onRunning() if stop is called when the datafileManager state is Starting', async () => { + const datafileManager = getMockDatafileManager({}); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + + manager.start(); + expect(manager.getState()).toBe(ServiceState.Starting); + manager.stop(); + await expect(manager.onRunning()).rejects.toThrow(); + }); + + it('should call datafileManager.stop()', async () => { + const datafileManager = getMockDatafileManager({}); + const spy = vi.spyOn(datafileManager, 'stop'); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + manager.stop(); + expect(spy).toHaveBeenCalled(); + }); + + it('should set status to Terminated immediately if no datafile manager is provided and resolve onTerminated', async () => { + const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig() }); + manager.stop(); + expect(manager.getState()).toBe(ServiceState.Terminated); + await expect(manager.onTerminated()).resolves.not.toThrow(); + }); + + it('should set status to Stopping while awaiting for datafileManager onTerminated', async () => { + const datafileManagerTerminated = resolvablePromise<void>(); + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await manager.onRunning(); + manager.stop(); + + for (let i = 0; i < 100; i++) { + expect(manager.getState()).toBe(ServiceState.Stopping); + await wait(0); + } + }); + + it('should set status to Terminated and resolve onTerminated after datafileManager.onTerminated() resolves', async () => { + const datafileManagerTerminated = resolvablePromise<void>(); + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await manager.onRunning(); + manager.stop(); + + for (let i = 0; i < 50; i++) { + expect(manager.getState()).toBe(ServiceState.Stopping); + await wait(0); + } + + datafileManagerTerminated.resolve(); + await expect(manager.onTerminated()).resolves.not.toThrow(); + expect(manager.getState()).toBe(ServiceState.Terminated); + }); + + it('should set status to Failed and reject onTerminated after datafileManager.onTerminated() rejects', async () => { + const datafileManagerTerminated = resolvablePromise<void>(); + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await manager.onRunning(); + manager.stop(); + + for (let i = 0; i < 50; i++) { + expect(manager.getState()).toBe(ServiceState.Stopping); + await wait(0); + } + + datafileManagerTerminated.reject(); + await expect(manager.onTerminated()).rejects.toThrow(); + expect(manager.getState()).toBe(ServiceState.Failed); + }); + + it('should not call onUpdate handlers after stop is called', async () => { + const datafileManagerTerminated = resolvablePromise<void>(); + const datafileManager = getMockDatafileManager({ onRunning: Promise.resolve(), onTerminated: datafileManagerTerminated.promise }); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.start(); + const listener = vi.fn(); + manager.onUpdate(listener); + + datafileManager.pushUpdate(testData.getTestProjectConfig()); + await manager.onRunning(); + + expect(listener).toHaveBeenCalledTimes(1); + manager.stop(); + datafileManager.pushUpdate(testData.getTestProjectConfigWithFeatures()); + + datafileManagerTerminated.resolve(); + await expect(manager.onTerminated()).resolves.not.toThrow(); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should make datafileManager disposable if makeDisposable() is called', async () => { + const datafileManager = getMockDatafileManager({}); + vi.spyOn(datafileManager, 'makeDisposable'); + const manager = new ProjectConfigManagerImpl({ datafileManager }); + manager.makeDisposable(); + + expect(datafileManager.makeDisposable).toHaveBeenCalled(); + }) + }); + }); +}); diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts new file mode 100644 index 000000000..8d7002c03 --- /dev/null +++ b/lib/project_config/project_config_manager.ts @@ -0,0 +1,237 @@ +/** + * Copyright 2019-2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from '../logging/logger'; +import { createOptimizelyConfig } from './optimizely_config'; +import { OptimizelyConfig } from '../shared_types'; +import { DatafileManager } from './datafile_manager'; +import { ProjectConfig, toDatafile, tryCreatingProjectConfig } from './project_config'; +import { Service, ServiceState, BaseService } from '../service'; +import { Consumer, Fn, Transformer } from '../utils/type'; +import { EventEmitter } from '../utils/event_emitter/event_emitter'; + +import { + SERVICE_FAILED_TO_START, + SERVICE_STOPPED_BEFORE_RUNNING, +} from '../service' + +export const NO_SDKKEY_OR_DATAFILE = 'sdkKey or datafile must be provided'; +export const GOT_INVALID_DATAFILE = 'got invalid datafile'; + +import { sprintf } from '../utils/fns'; +interface ProjectConfigManagerConfig { + datafile?: string | Record<string, unknown>; + jsonSchemaValidator?: Transformer<unknown, boolean>, + datafileManager?: DatafileManager; + logger?: LoggerFacade; +} + +export interface ProjectConfigManager extends Service { + setLogger(logger: LoggerFacade): void; + getConfig(): ProjectConfig | undefined; + getOptimizelyConfig(): OptimizelyConfig | undefined; + onUpdate(listener: Consumer<ProjectConfig>): Fn; +} + +/** + * ProjectConfigManager provides project config objects via its methods + * getConfig and onUpdate. It uses a DatafileManager to fetch datafile if provided. + * It is responsible for parsing and validating datafiles, and converting datafile + * string into project config objects. + * @param {ProjectConfigManagerConfig} config + */ + +export const LOGGER_NAME = 'ProjectConfigManager'; + +export class ProjectConfigManagerImpl extends BaseService implements ProjectConfigManager { + private datafile?: string | object; + private projectConfig?: ProjectConfig; + private optimizelyConfig?: OptimizelyConfig; + public jsonSchemaValidator?: Transformer<unknown, boolean>; + public datafileManager?: DatafileManager; + private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter(); + + constructor(config: ProjectConfigManagerConfig) { + super(); + this.jsonSchemaValidator = config.jsonSchemaValidator; + this.datafile = config.datafile; + this.datafileManager = config.datafileManager; + + if (config.logger) { + this.setLogger(config.logger); + } + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + this.logger.setName(LOGGER_NAME); + this.datafileManager?.setLogger(logger.child()); + } + + start(): void { + if (!this.isNew()) { + return; + } + + this.state = ServiceState.Starting; + + if (!this.datafile && !this.datafileManager) { + this.handleInitError(new Error(NO_SDKKEY_OR_DATAFILE)); + return; + } + + if (this.datafile) { + this.handleNewDatafile(this.datafile, true); + } + + this.datafileManager?.start(); + + // This handles the case where the datafile manager starts successfully. The + // datafile manager will only start successfully when it has downloaded a datafile, + // an will fire an onUpdate event. + this.datafileManager?.onUpdate(this.handleNewDatafile.bind(this)); + + // If the datafile manager runs successfully, it will emit a onUpdate event. We can + // handle the success case in the onUpdate handler. Hanlding the error case in the + // catch callback + this.datafileManager?.onRunning().catch((err) => { + this.handleDatafileManagerError(err); + }); + } + + makeDisposable(): void { + super.makeDisposable(); + this.datafileManager?.makeDisposable(); + } + + private handleInitError(error: Error): void { + this.logger?.error(error); + this.state = ServiceState.Failed; + this.datafileManager?.stop(); + this.startPromise.reject(error); + this.stopPromise.reject(error); + } + + private handleDatafileManagerError(err: Error): void { + this.logger?.error(SERVICE_FAILED_TO_START, 'DatafileManager', err.message); + + // If datafile manager onRunning() promise is rejected, and the project config manager + // is still in starting state, that means a datafile was not provided in cofig or was invalid, + // otherwise the state would have already been set to running synchronously. + // In this case, we cannot recover. + if (this.isStarting()) { + this.handleInitError(new Error( + sprintf(SERVICE_FAILED_TO_START, 'DatafileManager', err.message) + )); + } + } + + /** + * Handle new datafile by attemping to create a new Project Config object. If successful and + * the new config object's revision is newer than the current one, sets/updates the project config + * and emits onUpdate event. If unsuccessful, + * the project config and optimizely config objects will not be updated. If the error + * is fatal, handleInitError will be called. + */ + private handleNewDatafile(newDatafile: string | object, fromConfig = false): void { + if (this.isDone()) { + return; + } + + try { + const config = tryCreatingProjectConfig({ + datafile: newDatafile, + jsonSchemaValidator: this.jsonSchemaValidator, + logger: this.logger, + }); + + if(this.isStarting()) { + this.state = ServiceState.Running; + this.startPromise.resolve(); + } + + if (this.projectConfig?.revision !== config.revision) { + this.projectConfig = config; + this.optimizelyConfig = undefined; + this.eventEmitter.emit('update', config); + } + } catch (err) { + this.logger?.error(err); + + // if the state is starting and no datafileManager is provided, we cannot recover. + // If the state is starting and the datafileManager has emitted a datafile, + // that means a datafile was not provided in config or an invalid datafile was provided, + // otherwise the state would have already been set to running synchronously. + // If the first datafile emitted by the datafileManager is invalid, + // we consider this to be an initialization error as well. + const fatalError = (this.isStarting() && !this.datafileManager) || + (this.isStarting() && !fromConfig); + if (fatalError) { + this.handleInitError(new Error(GOT_INVALID_DATAFILE)); + } + } + } + + getConfig(): ProjectConfig | undefined { + return this.projectConfig; + } + + getOptimizelyConfig(): OptimizelyConfig | undefined { + if (!this.optimizelyConfig && this.projectConfig) { + this.optimizelyConfig = createOptimizelyConfig(this.projectConfig, toDatafile(this.projectConfig), this.logger); + } + return this.optimizelyConfig; + } + + /** + * Add a listener for project config updates. The listener will be called + * whenever this instance has a new project config object available. + * Returns a dispose function that removes the subscription + * @param {Function} listener + * @return {Function} + */ + onUpdate(listener: Consumer<ProjectConfig>): Fn { + return this.eventEmitter.on('update', listener); + } + + stop(): void { + if (this.isDone()) { + return; + } + + if (this.isNew() || this.isStarting()) { + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'ProjectConfigManager') + )); + } + + this.state = ServiceState.Stopping; + this.eventEmitter.removeAllListeners(); + if (!this.datafileManager) { + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + return; + } + + this.datafileManager.stop(); + this.datafileManager.onTerminated().then(() => { + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + }).catch((err) => { + this.state = ServiceState.Failed; + this.stopPromise.reject(err); + }); + } +} diff --git a/lib/project_config/project_config_schema.ts b/lib/project_config/project_config_schema.ts new file mode 100644 index 000000000..f842179dc --- /dev/null +++ b/lib/project_config/project_config_schema.ts @@ -0,0 +1,318 @@ +/** + * Copyright 2016-2017, 2020, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*eslint-disable */ +/** + * Project Config JSON Schema file used to validate the project json datafile + */ +import { JSONSchema4 } from 'json-schema'; + +var schemaDefinition = { + $schema: 'http://json-schema.org/draft-04/schema#', + title: 'Project Config JSON Schema', + type: 'object', + properties: { + projectId: { + type: 'string', + required: true, + }, + accountId: { + type: 'string', + required: true, + }, + groups: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + policy: { + type: 'string', + required: true, + }, + trafficAllocation: { + type: 'array', + items: { + type: 'object', + properties: { + entityId: { + type: 'string', + required: true, + }, + endOfRange: { + type: 'integer', + required: true, + }, + }, + }, + required: true, + }, + experiments: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + key: { + type: 'string', + required: true, + }, + status: { + type: 'string', + required: true, + }, + layerId: { + type: 'string', + required: true, + }, + variations: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + key: { + type: 'string', + required: true, + }, + }, + }, + required: true, + }, + trafficAllocation: { + type: 'array', + items: { + type: 'object', + properties: { + entityId: { + type: 'string', + required: true, + }, + endOfRange: { + type: 'integer', + required: true, + }, + }, + }, + required: true, + }, + audienceIds: { + type: 'array', + items: { + type: 'string', + }, + required: true, + }, + forcedVariations: { + type: 'object', + required: true, + }, + }, + }, + required: true, + }, + }, + }, + required: true, + }, + experiments: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + key: { + type: 'string', + required: true, + }, + status: { + type: 'string', + required: true, + }, + layerId: { + type: 'string', + required: true, + }, + variations: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + key: { + type: 'string', + required: true, + }, + }, + }, + required: true, + }, + trafficAllocation: { + type: 'array', + items: { + type: 'object', + properties: { + entityId: { + type: 'string', + required: true, + }, + endOfRange: { + type: 'integer', + required: true, + }, + }, + }, + required: true, + }, + audienceIds: { + type: 'array', + items: { + type: 'string', + }, + required: true, + }, + forcedVariations: { + type: 'object', + required: true, + }, + cmab: { + type: 'object', + required: false, + properties: { + attributes: { + type: 'array', + items: { + type: 'string', + }, + required: true, + } + } + } + }, + }, + required: true, + }, + events: { + type: 'array', + items: { + type: 'object', + properties: { + key: { + type: 'string', + required: true, + }, + experimentIds: { + type: 'array', + items: { + type: 'string', + required: true, + }, + }, + id: { + type: 'string', + required: true, + }, + }, + }, + required: true, + }, + audiences: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + name: { + type: 'string', + required: true, + }, + conditions: { + type: 'string', + required: true, + }, + }, + }, + required: true, + }, + attributes: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + required: true, + }, + key: { + type: 'string', + required: true, + }, + }, + }, + required: true, + }, + version: { + type: 'string', + required: true, + }, + revision: { + type: 'string', + required: true, + }, + integrations: { + type: 'array', + items: { + type: 'object', + properties: { + key: { + type: 'string', + required: true + }, + host: { + type: 'string' + }, + publicKey: { + type: 'string' + }, + pixelUrl: { + type: 'string' + }, + }, + }, + }, + }, +}; + +const schema = schemaDefinition as JSONSchema4 + +export default schema diff --git a/lib/service.spec.ts b/lib/service.spec.ts new file mode 100644 index 000000000..0b9d7c754 --- /dev/null +++ b/lib/service.spec.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { it, expect } from 'vitest'; +import { BaseService, ServiceState, StartupLog } from './service'; +import { LogLevel } from './logging/logger'; +import { getMockLogger } from './tests/mock/mock_logger'; +class TestService extends BaseService { + constructor(startUpLogs?: StartupLog[]) { + super(startUpLogs); + } + + start(): void { + super.start(); + this.setState(ServiceState.Running); + this.startPromise.resolve(); + } + + failStart(): void { + this.setState(ServiceState.Failed); + this.startPromise.reject(); + } + + stop(): void { + this.setState(ServiceState.Running); + this.startPromise.resolve(); + } + + failStop(): void { + this.setState(ServiceState.Failed); + this.startPromise.reject(); + } + + setState(state: ServiceState): void { + this.state = state; + } +} + + +it('should set state to New on construction', async () => { + const service = new TestService(); + expect(service.getState()).toBe(ServiceState.New); +}); + +it('should return correct state when getState() is called', () => { + const service = new TestService(); + expect(service.getState()).toBe(ServiceState.New); + service.setState(ServiceState.Running); + expect(service.getState()).toBe(ServiceState.Running); + service.setState(ServiceState.Terminated); + expect(service.getState()).toBe(ServiceState.Terminated); + service.setState(ServiceState.Failed); + expect(service.getState()).toBe(ServiceState.Failed); +}); + +it('should log startupLogs on start', () => { + const startUpLogs: StartupLog[] = [ + { + level: LogLevel.Warn, + message: 'warn message', + params: [1, 2] + }, + { + level: LogLevel.Error, + message: 'error message', + params: [3, 4] + }, + ]; + + const logger = getMockLogger(); + const service = new TestService(startUpLogs); + service.setLogger(logger); + service.start(); + + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith('warn message', 1, 2); + expect(logger.error).toHaveBeenCalledWith('error message', 3, 4); +}); + +it('should return an appropraite promise when onRunning() is called', () => { + const service1 = new TestService(); + const onRunning1 = service1.onRunning(); + + const service2 = new TestService(); + const onRunning2 = service2.onRunning(); + + return new Promise<void>((done) => { + Promise.all([ + onRunning1.then(() => { + expect(service1.getState()).toBe(ServiceState.Running); + }), onRunning2.catch(() => { + expect(service2.getState()).toBe(ServiceState.Failed); + }) + ]).then(() => done()); + + service1.start(); + service2.failStart(); + }); +}); + +it('should return an appropraite promise when onRunning() is called', () => { + const service1 = new TestService(); + const onRunning1 = service1.onRunning(); + + const service2 = new TestService(); + const onRunning2 = service2.onRunning(); + + return new Promise<void>((done) => { + Promise.all([ + onRunning1.then(() => { + expect(service1.getState()).toBe(ServiceState.Running); + }), onRunning2.catch(() => { + expect(service2.getState()).toBe(ServiceState.Failed); + }) + ]).then(() => done()); + + service1.start(); + service2.failStart(); + }); +}); diff --git a/lib/service.ts b/lib/service.ts new file mode 100644 index 000000000..3022aa806 --- /dev/null +++ b/lib/service.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2024-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LoggerFacade, LogLevel, LogLevelToLower } from './logging/logger' +import { resolvablePromise, ResolvablePromise } from "./utils/promise/resolvablePromise"; + +export const SERVICE_FAILED_TO_START = '%s failed to start, reason: %s'; +export const SERVICE_STOPPED_BEFORE_RUNNING = '%s stopped before running'; + +/** + * The service interface represents an object with an operational state, + * with methods to start and stop. The design of this interface in modelled + * after Guava Service interface (https://github.com/google/guava/wiki/ServiceExplained). + */ + +export enum ServiceState { + New, + Starting, + Running, + Stopping, + Terminated, + Failed, +} + +export type StartupLog = { + level: LogLevel; + message: string; + params: any[]; +} + +export interface Service { + getState(): ServiceState; + start(): void; + // onRunning will reject if the service fails to start + // or stopped before it could start. + // It will resolve if the service is starts successfully. + onRunning(): Promise<void>; + stop(): void; + // onTerminated will reject if the service enters a failed state + // either by failing to start or stop. + // It will resolve if the service is stopped successfully. + onTerminated(): Promise<void>; + makeDisposable(): void; +} + +export abstract class BaseService implements Service { + protected state: ServiceState; + protected startPromise: ResolvablePromise<void>; + protected stopPromise: ResolvablePromise<void>; + protected logger?: LoggerFacade; + protected startupLogs: StartupLog[]; + protected disposable = false; + constructor(startupLogs: StartupLog[] = []) { + this.state = ServiceState.New; + this.startPromise = resolvablePromise(); + this.stopPromise = resolvablePromise(); + this.startupLogs = startupLogs; + + // avoid unhandled promise rejection + this.startPromise.promise.catch(() => {}); + this.stopPromise.promise.catch(() => {}); + } + + makeDisposable(): void { + this.disposable = true; + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + } + + protected printStartupLogs(): void { + if (!this.logger) { + return; + } + + for (const { level, message, params } of this.startupLogs) { + const methodName: string = LogLevelToLower[level]; + const method = this.logger[methodName as keyof LoggerFacade]; + method.call(this.logger, message, ...params); + } + } + + onRunning(): Promise<void> { + return this.startPromise.promise; + } + + onTerminated(): Promise<void> { + return this.stopPromise.promise; + } + + getState(): ServiceState { + return this.state; + } + + isStarting(): boolean { + return this.state === ServiceState.Starting; + } + + isRunning(): boolean { + return this.state === ServiceState.Running; + } + + isNew(): boolean { + return this.state === ServiceState.New; + } + + isDone(): boolean { + return [ + ServiceState.Stopping, + ServiceState.Terminated, + ServiceState.Failed + ].includes(this.state); + } + + start(): void { + this.printStartupLogs(); + } + + abstract stop(): void; +} diff --git a/lib/shared_types.ts b/lib/shared_types.ts new file mode 100644 index 000000000..45e66413b --- /dev/null +++ b/lib/shared_types.ts @@ -0,0 +1,529 @@ +/** + * Copyright 2020-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file contains the shared type definitions collected from the SDK. + * These shared type definitions include ones that will be referenced by external consumers via export_types.ts. + */ + +// import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from './modules/logging'; +import { LoggerFacade, LogLevel } from './logging/logger'; +import { ErrorHandler } from './error/error_handler'; + +import { NotificationCenter, DefaultNotificationCenter } from './notification_center'; + +import { IOptimizelyUserContext as OptimizelyUserContext } from './optimizely_user_context'; + +import { RequestHandler } from './utils/http_request_handler/http'; +import { OptimizelySegmentOption } from './odp/segment_manager/optimizely_segment_option'; +import { OdpSegmentApiManager } from './odp/segment_manager/odp_segment_api_manager'; +import { OdpSegmentManager } from './odp/segment_manager/odp_segment_manager'; +import { DefaultOdpEventApiManager } from './odp/event_manager/odp_event_api_manager'; +import { OdpEventManager } from './odp/event_manager/odp_event_manager'; +import { OdpManager } from './odp/odp_manager'; +import { ProjectConfig } from './project_config/project_config'; +import { OpaqueConfigManager } from './project_config/config_manager_factory'; +import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; +import { EventProcessor } from './event_processor/event_processor'; +import { VuidManager } from './vuid/vuid_manager'; +import { ErrorNotifier } from './error/error_notifier'; +import { OpaqueLogger } from './logging/logger_factory'; +import { OpaqueErrorNotifier } from './error/error_notifier_factory'; +import { OpaqueEventProcessor } from './event_processor/event_processor_factory'; +import { OpaqueOdpManager } from './odp/odp_manager_factory'; +import { OpaqueVuidManager } from './vuid/vuid_manager_factory'; + +export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; +export { EventProcessor } from './event_processor/event_processor'; +export { NotificationCenter } from './notification_center'; +export { VuidManager } from './vuid/vuid_manager'; +export { OpaqueLogger } from './logging/logger_factory'; +export { OpaqueErrorNotifier } from './error/error_notifier_factory'; + +export interface BucketerParams { + experimentId: string; + experimentKey: string; + userId: string; + trafficAllocationConfig: TrafficAllocation[]; + experimentKeyMap: { [key: string]: Experiment }; + experimentIdMap: { [id: string]: Experiment }; + groupIdMap: { [key: string]: Group }; + variationIdMap: { [id: string]: Variation }; + logger?: LoggerFacade; + bucketingId: string; + validateEntity?: boolean; +} + +export interface DecisionResponse<T> { + readonly error?: boolean; + readonly result: T; + readonly reasons: [string, ...any[]][]; +} + +export type UserAttributeValue = string | number | boolean | null | undefined | ExperimentBucketMap; + +export type UserAttributes = { + $opt_bucketing_id?: string; + $opt_experiment_bucket_map?: ExperimentBucketMap; + [name: string]: UserAttributeValue; +}; + +export interface ExperimentBucketMap { + [experiment_id: string]: { variation_id: string }; +} + +// Information about past bucketing decisions for a user. +export interface UserProfile { + user_id: string; + experiment_bucket_map: ExperimentBucketMap; +} + +export type EventTags = { + revenue?: string | number | null; + value?: string | number | null; + $opt_event_properties?: Record<string, unknown>; + [key: string]: unknown; +}; + +export interface UserProfileService { + lookup(userId: string): UserProfile; + save(profile: UserProfile): void; +} + +export interface UserProfileServiceAsync { + lookup(userId: string): Promise<UserProfile>; + save(profile: UserProfile): Promise<void>; +} + +export interface DatafileManagerConfig { + sdkKey: string; + datafile?: string; +} + +export interface DatafileOptions { + autoUpdate?: boolean; + updateInterval?: number; + urlTemplate?: string; + datafileAccessToken?: string; +} + +export interface ListenerPayload { + userId: string; + attributes?: UserAttributes; +} + +// An event to be submitted to Optimizely, enabling tracking the reach and impact of +// tests and feature rollouts. +export interface Event { + // URL to which to send the HTTP request. + url: string; + // HTTP method with which to send the event. + httpVerb: 'POST'; + // Value to send in the request body, JSON-serialized. + // TODO[OASIS-6649]: Don't use any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params: any; +} + +export interface VariationVariable { + id: string; + value: string; +} + +export interface Variation { + id: string; + key: string; + featureEnabled?: boolean; + variablesMap: OptimizelyVariablesMap; + variables?: VariationVariable[]; +} + +export interface ExperimentCore { + id: string; + key: string; + variations: Variation[]; + variationKeyMap: { [key: string]: Variation }; + audienceConditions: Array<string | string[]>; + audienceIds: string[]; + trafficAllocation: TrafficAllocation[]; +} + +export interface Experiment extends ExperimentCore { + layerId: string; + groupId?: string; + status: string; + forcedVariations?: { [key: string]: string }; + isRollout?: boolean; + cmab?: { + trafficAllocation: number; + attributeIds: string[]; + }; +} + +export type HoldoutStatus = 'Draft' | 'Running' | 'Concluded' | 'Archived'; + +export interface Holdout extends ExperimentCore { + status: HoldoutStatus; + includedFlags: string[]; + excludedFlags: string[]; +} + +export function isHoldout(obj: Experiment | Holdout): obj is Holdout { + // Holdout has 'status', 'includedFlags', and 'excludedFlags' properties + return ( + (obj as Holdout).status !== undefined && + Array.isArray((obj as Holdout).includedFlags) && + Array.isArray((obj as Holdout).excludedFlags) + ); +} + +export enum VariableType { + BOOLEAN = 'boolean', + DOUBLE = 'double', + INTEGER = 'integer', + STRING = 'string', + JSON = 'json', +} + +export interface FeatureVariable { + type: VariableType; + key: string; + id: string; + defaultValue: string; + subType?: string; +} + +export interface FeatureFlag { + rolloutId: string; + key: string; + id: string; + experimentIds: string[]; + variables: FeatureVariable[]; + variableKeyMap: { [key: string]: FeatureVariable }; + groupId?: string; +} + +export type Condition = { + name: string; + type: string; + match?: string; + value: string | number | boolean | null; +}; + +export interface Audience { + id: string; + name: string; + conditions: unknown[] | string; +} + +export interface Integration { + key: string; + host?: string; + publicKey?: string; + pixelUrl?: string; +} + +export interface TrafficAllocation { + entityId: string; + endOfRange: number; +} + +export interface Group { + id: string; + policy: string; + trafficAllocation: TrafficAllocation[]; + experiments: Experiment[]; +} + +export interface TrafficAllocation { + entityId: string; + endOfRange: number; +} + +export interface Group { + id: string; + policy: string; + trafficAllocation: TrafficAllocation[]; + experiments: Experiment[]; +} + +export interface FeatureKeyMap { + [key: string]: FeatureFlag; +} + +export interface OnReadyResult { + success: boolean; + reason?: string; +} + +export type ObjectWithUnknownProperties = { + [key: string]: unknown; +}; + +export interface Rollout { + id: string; + experiments: Experiment[]; +} + +//TODO: Move OptimizelyDecideOption to @optimizely/optimizely-sdk/lib/utils/enums +export enum OptimizelyDecideOption { + DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT', + ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY', + IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE', + INCLUDE_REASONS = 'INCLUDE_REASONS', + EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES', + IGNORE_CMAB_CACHE = 'IGNORE_CMAB_CACHE', + RESET_CMAB_CACHE = 'RESET_CMAB_CACHE', + INVALIDATE_USER_CMAB_CACHE = 'INVALIDATE_USER_CMAB_CACHE', +} + +/** + * Optimizely Config Entities + */ +export interface OptimizelyExperiment { + id: string; + key: string; + audiences: string; + variationsMap: { + [variationKey: string]: OptimizelyVariation; + }; +} + +export type FeatureVariableValue = number | string | boolean | object | null; + +export interface OptimizelyVariable { + id: string; + key: string; + type: string; + value: string; +} + +export interface Client { + // TODO: In the future, will add a function to allow overriding the VUID. + getVuid(): string | undefined; + createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext; + notificationCenter: NotificationCenter; + activate(experimentKey: string, userId: string, attributes?: UserAttributes): string | null; + track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void; + getVariation(experimentKey: string, userId: string, attributes?: UserAttributes): string | null; + setForcedVariation(experimentKey: string, userId: string, variationKey: string | null): boolean; + getForcedVariation(experimentKey: string, userId: string): string | null; + isFeatureEnabled(featureKey: string, userId: string, attributes?: UserAttributes): boolean; + getEnabledFeatures(userId: string, attributes?: UserAttributes): string[]; + getFeatureVariable( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): FeatureVariableValue; + getFeatureVariableBoolean( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): boolean | null; + getFeatureVariableDouble( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): number | null; + getFeatureVariableInteger( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): number | null; + getFeatureVariableString( + featureKey: string, + variableKey: string, + userId: string, + attributes?: UserAttributes + ): string | null; + getFeatureVariableJSON(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): unknown; + getAllFeatureVariables( + featureKey: string, + userId: string, + attributes?: UserAttributes + ): { [variableKey: string]: unknown } | null; + getOptimizelyConfig(): OptimizelyConfig | null; + onReady(options?: { timeout?: number }): Promise<unknown>; + close(): Promise<unknown>; + sendOdpEvent(action: string, type?: string, identifiers?: Map<string, string>, data?: Map<string, unknown>): void; + isOdpIntegrated(): boolean; +} + +export interface ActivateListenerPayload { + [key: string]: any; +} + +export interface TrackListenerPayload { + [key: string]: any; +} + +/** + * Entry level Config Entities + * For compatibility with the previous declaration file + */ +export interface Config { + projectConfigManager: OpaqueConfigManager; + eventProcessor?: OpaqueEventProcessor; + // The object to validate against the schema + jsonSchemaValidator?: { + validate(jsonObject: unknown): boolean; + }; + logger?: OpaqueLogger; + errorNotifier?: OpaqueErrorNotifier; + // user profile that contains user information + userProfileService?: UserProfileService; + userProfileServiceAsync?: UserProfileServiceAsync; + // dafault options for decide API + defaultDecideOptions?: OptimizelyDecideOption[]; + clientEngine?: string; + clientVersion?: string; + odpManager?: OpaqueOdpManager; + vuidManager?: OpaqueVuidManager; + disposable?: boolean; +} + +export type OptimizelyExperimentsMap = { + [experimentKey: string]: OptimizelyExperiment; +}; + +export type OptimizelyVariablesMap = { + [variableKey: string]: OptimizelyVariable; +}; + +export type OptimizelyFeaturesMap = { + [featureKey: string]: OptimizelyFeature; +}; + +export type OptimizelyAttribute = { + id: string; + key: string; +}; + +export type OptimizelyAudience = { + id: string; + name: string; + conditions: string; +}; + +export type OptimizelyEvent = { + id: string; + key: string; + experimentIds: string[]; +}; + +export interface OptimizelyFeature { + id: string; + key: string; + experimentRules: OptimizelyExperiment[]; + deliveryRules: OptimizelyExperiment[]; + variablesMap: OptimizelyVariablesMap; + + /** + * @deprecated Use experimentRules and deliveryRules + */ + experimentsMap: OptimizelyExperimentsMap; +} + +export interface OptimizelyVariation { + id: string; + key: string; + featureEnabled?: boolean; + variablesMap: OptimizelyVariablesMap; +} + +export interface OptimizelyConfig { + environmentKey: string; + sdkKey: string; + revision: string; + + /** + * This experimentsMap is for experiments of legacy projects only. + * For flag projects, experiment keys are not guaranteed to be unique + * across multiple flags, so this map may not include all experiments + * when keys conflict. + */ + experimentsMap: OptimizelyExperimentsMap; + + featuresMap: OptimizelyFeaturesMap; + attributes: OptimizelyAttribute[]; + audiences: OptimizelyAudience[]; + events: OptimizelyEvent[]; + getDatafile(): string; +} + +export { OptimizelyUserContext }; + +export interface OptimizelyDecision { + variationKey: string | null; + // The boolean value indicating if the flag is enabled or not + enabled: boolean; + // The collection of variables associated with the decision + variables: { [variableKey: string]: unknown }; + // The rule key of the decision + ruleKey: string | null; + // The flag key for which the decision has been made for + flagKey: string; + // A copy of the user context for which the decision has been made for + userContext: OptimizelyUserContext; + // An array of error/info messages describing why the decision has been made. + reasons: string[]; +} + +export interface DatafileUpdate { + datafile: string; +} + +export interface DatafileUpdateListener { + (datafileUpdate: DatafileUpdate): void; +} + +// TODO: Replace this with the one from js-sdk-models +interface Managed { + start(): void; + + stop(): Promise<unknown>; +} + +export interface DatafileManager extends Managed { + get: () => string; + on(eventName: string, listener: DatafileUpdateListener): () => void; + onReady: () => Promise<void>; +} + +export interface OptimizelyDecisionContext { + flagKey: string; + ruleKey?: string; +} + +export interface OptimizelyForcedDecision { + variationKey: string; +} + +// ODP Exports + +export { + RequestHandler, + OptimizelySegmentOption, + OdpSegmentApiManager, + OdpSegmentManager, + DefaultOdpEventApiManager, + OdpEventManager, + OdpManager, +}; diff --git a/lib/tests/decision_test_datafile.ts b/lib/tests/decision_test_datafile.ts new file mode 100644 index 000000000..5048d2549 --- /dev/null +++ b/lib/tests/decision_test_datafile.ts @@ -0,0 +1,553 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// flag id starts from 1000 +// experiment id starts from 2000 +// rollout experiment id starts from 3000 +// audience id starts from 4000 +// variation id starts from 5000 +// variable id starts from 6000 +// attribute id starts from 7000 + +const testDatafile = { + accountId: "24535200037", + projectId: "5088239376138240", + revision: "21", + attributes: [ + { + id: "7001", + key: "age" + } + ], + audiences: [ + { + name: "age_22", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4001" + }, + { + name: "age_60", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4002" + }, + { + name: "age_90", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4003" + }, + { + name: "age_94", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4004" + }, + { + name: "age_95", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4005" + }, + { + name: "age_96", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4006" + }, + { + name: "age_97", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "4007" + }, + { + id: "$opt_dummy_audience", + name: "Optimizely-Generated Audience for Backwards Compatibility", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]" + } + ], + version: "4", + events: [], + integrations: [], + anonymizeIP: true, + botFiltering: false, + typedAudiences: [ + { + name: "age_22", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 22 + } + ] + ] + ], + id: "4001" + }, + { + name: "age_60", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 60 + } + ] + ] + ], + id: "4002" + }, + { + name: "age_90", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 90 + } + ] + ] + ], + id: "4003" + }, + { + name: "age_94", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 94 + } + ] + ] + ], + id: "4004" + }, + { + name: "age_95", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 95 + } + ] + ] + ], + id: "4005" + }, + { + name: "age_96", + conditions: [ + "and", + [ + "or", + [ + "or", + { + "match": "le", + name: "age", + "type": "custom_attribute", + value: 96 + } + ] + ] + ], + id: "4006" + }, + ], + variables: [], + environmentKey: "production", + sdkKey: "sdk_key", + featureFlags: [ + { + id: "1001", + key: "flag_1", + rolloutId: "rollout-371334-671741182375276", + experimentIds: [ + "2001", + "2002", + "2003" + ], + variables: [ + { + id: "6001", + key: "integer_variable", + "type": "integer", + "defaultValue": "0" + } + ] + }, + { + id: "1002", + key: "flag_2", + "rolloutId": "rollout-374517-931741182375293", + experimentIds: [ + "2004" + ], + "variables": [] + } + ], + "rollouts": [ + { + id: "rollout-371334-671741182375276", + experiments: [ + { + id: "3001", + key: "delivery_1", + status: "Running", + layerId: "9300001480454", + variations: [ + { + id: "5004", + key: "variation_4", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "4" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5004", + endOfRange: 1500 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4001" + ], + audienceConditions: [ + "or", + "4001" + ] + }, + { + id: "3002", + key: "delivery_2", + status: "Running", + layerId: "9300001480455", + variations: [ + { + id: "5005", + key: "variation_5", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "5" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5005", + endOfRange: 4000 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4002" + ], + audienceConditions: [ + "or", + "4002" + ] + }, + { + id: "3003", + key: "delivery_3", + status: "Running", + layerId: "9300001495996", + variations: [ + { + id: "5006", + key: "variation_6", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "6" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5006", + endOfRange: 8000 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4003" + ], + audienceConditions: [ + "or", + "4003" + ] + }, + { + id: "default-rollout-id", + key: "default-rollout-key", + status: "Running", + layerId: "rollout-371334-671741182375276", + variations: [ + { + id: "5007", + key: "variation_7", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "7" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5007", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [] + }, + ] + }, + { + id: "rollout-374517-931741182375293", + experiments: [ + { + id: "default-rollout-374517-931741182375293", + key: "default-rollout-374517-931741182375293", + status: "Running", + layerId: "rollout-374517-931741182375293", + variations: [ + { + id: "1177722", + key: "off", + featureEnabled: false, + variables: [] + } + ], + trafficAllocation: [ + { + "entityId": "1177722", + "endOfRange": 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [] + } + ] + }, + ], + experiments: [ + { + id: "2001", + key: "exp_1", + status: "Running", + layerId: "9300001480444", + variations: [ + { + id: "5001", + key: "variation_1", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "1" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5001", + endOfRange: 5000 + }, + { + entityId: "5001", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [ + "4001" + ], + audienceConditions: [ + "or", + "4001" + ] + }, + { + id: "2002", + key: "exp_2", + status: "Running", + layerId: "9300001480448", + variations: [ + { + id: "5002", + key: "variation_2", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "2" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5002", + endOfRange: 5000 + }, + { + entityId: "5002", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceConditions: [ + "or", + "4002" + ] + }, + { + id: "2003", + key: "exp_3", + status: "Running", + layerId: "9300001480451", + variations: [ + { + id: "5003", + key: "variation_3", + featureEnabled: true, + variables: [ + { + id: "6001", + value: "3" + } + ] + } + ], + trafficAllocation: [ + { + entityId: "5003", + endOfRange: 5000 + }, + { + entityId: "5003", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [ + "or", + "4003" + ], + cmab: { + attributes: ["7001"], + } + }, + { + id: "2004", + key: "exp_4", + status: "Running", + layerId: "9300001497754", + variations: [ + { + id: "5100", + key: "variation_flag_2", + featureEnabled: true, + variables: [] + } + ], + trafficAllocation: [ + { + entityId: "5100", + endOfRange: 5000 + }, + { + entityId: "5100", + endOfRange: 10000 + } + ], + forcedVariations: { + + }, + audienceIds: [], + audienceConditions: [] + } + ], + groups: [] +} + +export const getDecisionTestDatafile = (): any => { + return JSON.parse(JSON.stringify(testDatafile)); +} diff --git a/packages/optimizely-sdk/lib/plugins/logger/index.js b/lib/tests/exit_on_unhandled_rejection.js similarity index 61% rename from packages/optimizely-sdk/lib/plugins/logger/index.js rename to lib/tests/exit_on_unhandled_rejection.js index 31635d153..4722776cc 100644 --- a/packages/optimizely-sdk/lib/plugins/logger/index.js +++ b/lib/tests/exit_on_unhandled_rejection.js @@ -1,5 +1,5 @@ /** - * Copyright 2016-2017, Optimizely + * Copyright 2019, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var logging = require('@optimizely/js-sdk-logging'); -function NoOpLogger() {} - -NoOpLogger.prototype.log = function() {}; - -module.exports = { - createLogger: function(opts) { - return new logging.ConsoleLogHandler(opts); - }, - - createNoOpLogger: function() { - return new NoOpLogger(); - }, -}; +/* + * This is to stop & fail tests when an unhandled promise rejection occurs. + * See: https://nodejs.org/api/process.html#process_event_unhandledrejection + */ +process.on('unhandledRejection', function(err) { + console.error('Unhandled promise rejection'); + if (err) { + console.error(err); + } + process.exit(1); +}); diff --git a/lib/tests/mock/create_event.ts b/lib/tests/mock/create_event.ts new file mode 100644 index 000000000..ec5dd9949 --- /dev/null +++ b/lib/tests/mock/create_event.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function createImpressionEvent(id = 'uuid'): any { + return { + type: 'impression' as const, + timestamp: 69, + uuid: id, + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } +} \ No newline at end of file diff --git a/lib/tests/mock/mock_cache.ts b/lib/tests/mock/mock_cache.ts new file mode 100644 index 000000000..21a89e7a4 --- /dev/null +++ b/lib/tests/mock/mock_cache.ts @@ -0,0 +1,124 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SyncCache, AsyncCache } from "../../utils/cache/cache"; +import { SyncStore, AsyncStore } from "../../utils/cache/store"; +import { Maybe } from "../../utils/type"; + +type SyncCacheWithAddOn<T> = SyncCache<T> & { + size(): number; + getAll(): Map<string, T>; +}; + +type AsyncCacheWithAddOn<T> = AsyncCache<T> & { + size(): Promise<number>; + getAll(): Promise<Map<string, T>>; +}; + +type SyncStoreWithAddOn<T> = SyncStore<T> & { + size(): number; + getAll(): Map<string, T>; +}; + +type AsyncStoreWithAddOn<T> = AsyncStore<T> & { + size(): Promise<number>; + getAll(): Promise<Map<string, T>>; +}; + +export const getMockSyncCache = <T>(): SyncCacheWithAddOn<T> & SyncStoreWithAddOn<T> => { + const cache = { + operation: 'sync' as const, + data: new Map<string, T>(), + remove(key: string): void { + this.data.delete(key); + }, + clear(): void { + this.data.clear(); + }, + reset(): void { + this.clear(); + }, + getKeys(): string[] { + return Array.from(this.data.keys()); + }, + getAll(): Map<string, T> { + return this.data; + }, + getBatched(keys: string[]): Maybe<T>[] { + return keys.map((key) => this.get(key)); + }, + size(): number { + return this.data.size; + }, + get(key: string): T | undefined { + return this.data.get(key); + }, + lookup(key: string): T | undefined { + return this.get(key); + }, + set(key: string, value: T): void { + this.data.set(key, value); + }, + save(key: string, value: T): void { + this.data.set(key, value); + } + } + + return cache; +}; + + +export const getMockAsyncCache = <T>(): AsyncCacheWithAddOn<T> & AsyncStoreWithAddOn<T> => { + const cache = { + operation: 'async' as const, + data: new Map<string, T>(), + async remove(key: string): Promise<void> { + this.data.delete(key); + }, + async clear(): Promise<void> { + this.data.clear(); + }, + async reset(): Promise<void> { + this.clear(); + }, + async getKeys(): Promise<string[]> { + return Array.from(this.data.keys()); + }, + async getAll(): Promise<Map<string, T>> { + return this.data; + }, + async getBatched(keys: string[]): Promise<Maybe<T>[]> { + return Promise.all(keys.map((key) => this.get(key))); + }, + async size(): Promise<number> { + return this.data.size; + }, + async get(key: string): Promise<Maybe<T>> { + return this.data.get(key); + }, + async lookup(key: string): Promise<Maybe<T>> { + return this.get(key); + }, + async set(key: string, value: T): Promise<void> { + this.data.set(key, value); + }, + async save(key: string, value: T): Promise<void> { + return this.set(key, value); + } + } + + return cache; +}; diff --git a/lib/tests/mock/mock_datafile_manager.ts b/lib/tests/mock/mock_datafile_manager.ts new file mode 100644 index 000000000..1c9d66b38 --- /dev/null +++ b/lib/tests/mock/mock_datafile_manager.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Consumer } from '../../utils/type'; +import { DatafileManager } from '../../project_config/datafile_manager'; +import { EventEmitter } from '../../utils/event_emitter/event_emitter'; +import { BaseService } from '../../service'; +import { LoggerFacade } from '../../logging/logger'; + +type MockConfig = { + datafile?: string | object; + onRunning?: Promise<void>, + onTerminated?: Promise<void>, +} + +class MockDatafileManager extends BaseService implements DatafileManager { + eventEmitter: EventEmitter<{ update: string}> = new EventEmitter(); + datafile: string | object | undefined; + + constructor(opt: MockConfig) { + super(); + this.datafile = opt.datafile; + this.startPromise.resolve(opt.onRunning || Promise.resolve()); + this.stopPromise.resolve(opt.onTerminated || Promise.resolve()); + } + + start(): void { + return; + } + + stop(): void { + return; + } + + setLogger(logger: LoggerFacade): void { + } + + get(): string | undefined { + if (typeof this.datafile === 'object') { + return JSON.stringify(this.datafile); + } + return this.datafile; + } + + setDatafile(datafile: string): void { + this.datafile = datafile; + } + + onUpdate(listener: Consumer<string>): () => void { + return this.eventEmitter.on('update', listener) + } + + pushUpdate(datafile: string | object): void { + if (typeof datafile === 'object') { + datafile = JSON.stringify(datafile); + } + this.datafile = datafile; + this.eventEmitter.emit('update', datafile); + } +} + +export const getMockDatafileManager = (opt: MockConfig): MockDatafileManager => { + return new MockDatafileManager(opt); +}; diff --git a/lib/tests/mock/mock_logger.ts b/lib/tests/mock/mock_logger.ts new file mode 100644 index 000000000..e9d9cb4cf --- /dev/null +++ b/lib/tests/mock/mock_logger.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi } from 'vitest'; +import { LoggerFacade } from '../../logging/logger'; + +type MockFn = ReturnType<typeof vi.fn>; +type MockLogger = { + info: MockFn; + error: MockFn; + warn: MockFn; + debug: MockFn; + child: MockFn; + setName: MockFn; +}; + +export const getMockLogger = (): MockLogger => { + return { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + child: vi.fn().mockImplementation(() => getMockLogger()), + setName: vi.fn(), + }; +}; diff --git a/lib/tests/mock/mock_project_config_manager.ts b/lib/tests/mock/mock_project_config_manager.ts new file mode 100644 index 000000000..931f37da1 --- /dev/null +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProjectConfigManager } from '../../project_config/project_config_manager'; +import { ProjectConfig } from '../../project_config/project_config'; +import { Consumer } from '../../utils/type'; + +type MockOpt = { + initConfig?: ProjectConfig, + onRunning?: Promise<void>, + onTerminated?: Promise<void>, +} + +export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigManager => { + return { + disposable: false, + config: opt.initConfig, + makeDisposable(){ + this.disposable = true; + }, + start: () => {}, + onRunning: () => opt.onRunning || Promise.resolve(), + stop: () => {}, + onTerminated: () => opt.onTerminated || Promise.resolve(), + getConfig: function() { + return this.config; + }, + setConfig: function(config: ProjectConfig) { + this.config = config; + }, + onUpdate: function(listener: Consumer<ProjectConfig>) { + if (this.listeners === undefined) { + this.listeners = []; + } + this.listeners.push(listener); + return () => {}; + }, + pushUpdate: function(config: ProjectConfig) { + this.listeners.forEach((listener: any) => listener(config)); + }, + setLogger: function(logger: any) { + } + } as any as ProjectConfigManager; +}; diff --git a/lib/tests/mock/mock_repeater.ts b/lib/tests/mock/mock_repeater.ts new file mode 100644 index 000000000..f70b0b477 --- /dev/null +++ b/lib/tests/mock/mock_repeater.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi } from 'vitest'; +import { AsyncTransformer } from '../../utils/type'; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const getMockRepeater = () => { + const mock = { + running: false, + handler: undefined as any, + start: vi.fn(), + stop: vi.fn(), + reset: vi.fn(), + setTask(handler: AsyncTransformer<number, void>) { + this.handler = handler; + }, + // throw if not running. This ensures tests cannot + // do mock exection when the repeater is supposed to be not running. + execute(failureCount: number): Promise<void> { + if (!this.isRunning()) throw new Error(); + const ret = this.handler?.(failureCount); + ret?.catch(() => {}); + return ret; + }, + isRunning: () => mock.running, + }; + mock.start.mockImplementation(() => mock.running = true); + mock.stop.mockImplementation(() => mock.running = false); + return mock; +} diff --git a/lib/tests/mock/mock_request_handler.ts b/lib/tests/mock/mock_request_handler.ts new file mode 100644 index 000000000..3369bf125 --- /dev/null +++ b/lib/tests/mock/mock_request_handler.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi } from 'vitest'; +import { AbortableRequest, Response } from '../../utils/http_request_handler/http'; +import { ResolvablePromise, resolvablePromise } from '../../utils/promise/resolvablePromise'; + + +export type MockAbortableRequest = AbortableRequest & { + mockResponse: ResolvablePromise<Response>; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const getMockAbortableRequest = (res?: Promise<Response>) => { + const response = resolvablePromise<Response>(); + if (res) response.resolve(res); + return { + mockResponse: response, + responsePromise: response.promise, + abort: vi.fn(), + }; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const getMockRequestHandler = () => { + const mock = { + makeRequest: vi.fn(), + } + return mock; +} diff --git a/lib/tests/testUtils.ts b/lib/tests/testUtils.ts new file mode 100644 index 000000000..2a4cbe3c5 --- /dev/null +++ b/lib/tests/testUtils.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vi } from 'vitest'; + +export const exhaustMicrotasks = async (loop = 100): Promise<void> => { + for(let i = 0; i < loop; i++) { + await Promise.resolve(); + } +}; + +export const wait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms)); + +export const advanceTimersByTime = (waitMs: number): Promise<void> => { + const timeoutPromise: Promise<void> = new Promise(res => setTimeout(res, waitMs)); + vi.advanceTimersByTime(waitMs); + return timeoutPromise; +} diff --git a/lib/tests/test_data.ts b/lib/tests/test_data.ts new file mode 100644 index 000000000..e16081939 --- /dev/null +++ b/lib/tests/test_data.ts @@ -0,0 +1,4189 @@ +/** + * Copyright 2016-2021, 2024-2025 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +const cloneDeep = (x: any) => JSON.parse(JSON.stringify(x)); + +const config: any = { + revision: '42', + version: '2', + events: [ + { + key: 'testEvent', + experimentIds: ['111127'], + id: '111095', + }, + { + key: 'Total Revenue', + experimentIds: ['111127'], + id: '111096', + }, + { + key: 'testEventWithAudiences', + experimentIds: ['122227'], + id: '111097', + }, + { + key: 'testEventWithoutExperiments', + experimentIds: [], + id: '111098', + }, + { + key: 'testEventWithExperimentNotRunning', + experimentIds: ['133337'], + id: '111099', + }, + { + key: 'testEventWithMultipleExperiments', + experimentIds: ['111127', '122227', '133337'], + id: '111100', + }, + { + key: 'testEventLaunched', + experimentIds: ['144447'], + id: '111101', + }, + ], + groups: [ + { + id: '666', + policy: 'random', + trafficAllocation: [ + { + entityId: '442', + endOfRange: 3000, + }, + { + entityId: '443', + endOfRange: 6000, + }, + ], + experiments: [ + { + id: '442', + key: 'groupExperiment1', + status: 'Running', + variations: [ + { + id: '551', + key: 'var1exp1', + }, + { + id: '552', + key: 'var2exp1', + }, + ], + trafficAllocation: [ + { + entityId: '551', + endOfRange: 5000, + }, + { + entityId: '552', + endOfRange: 9000, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + audienceIds: ['11154'], + forcedVariations: {}, + layerId: '1', + }, + { + id: '443', + key: 'groupExperiment2', + status: 'Running', + variations: [ + { + id: '661', + key: 'var1exp2', + }, + { + id: '662', + key: 'var2exp2', + }, + ], + trafficAllocation: [ + { + entityId: '661', + endOfRange: 5000, + }, + { + entityId: '662', + endOfRange: 10000, + }, + ], + audienceIds: [], + forcedVariations: {}, + layerId: '2', + }, + ], + }, + { + id: '667', + policy: 'overlapping', + trafficAllocation: [], + experiments: [ + { + id: '444', + key: 'overlappingGroupExperiment1', + status: 'Running', + variations: [ + { + id: '553', + key: 'overlappingvar1', + }, + { + id: '554', + key: 'overlappingvar2', + }, + ], + trafficAllocation: [ + { + entityId: '553', + endOfRange: 1500, + }, + { + entityId: '554', + endOfRange: 3000, + }, + ], + audienceIds: [], + forcedVariations: {}, + layerId: '3', + }, + ], + }, + ], + experiments: [ + { + key: 'testExperiment', + status: 'Running', + forcedVariations: { + user1: 'control', + user2: 'variation', + }, + audienceIds: [], + layerId: '4', + trafficAllocation: [ + { + entityId: '111128', + endOfRange: 4000, + }, + { + entityId: '111129', + endOfRange: 9000, + }, + ], + id: '111127', + variations: [ + { + key: 'control', + id: '111128', + }, + { + key: 'variation', + id: '111129', + }, + ], + }, + { + key: 'testExperimentWithAudiences', + status: 'Running', + forcedVariations: { + user1: 'controlWithAudience', + user2: 'variationWithAudience', + }, + audienceIds: ['11154'], + layerId: '5', + trafficAllocation: [ + { + entityId: '122228', + endOfRange: 4000, + }, + { + entityId: '122229', + endOfRange: 10000, + }, + ], + id: '122227', + variations: [ + { + key: 'controlWithAudience', + id: '122228', + }, + { + key: 'variationWithAudience', + id: '122229', + }, + ], + }, + { + key: 'testExperimentNotRunning', + status: 'Not started', + forcedVariations: { + user1: 'controlNotRunning', + user2: 'variationNotRunning', + }, + audienceIds: [], + layerId: '6', + trafficAllocation: [ + { + entityId: '133338', + endOfRange: 4000, + }, + { + entityId: '133339', + endOfRange: 10000, + }, + ], + id: '133337', + variations: [ + { + key: 'controlNotRunning', + id: '133338', + }, + { + key: 'variationNotRunning', + id: '133339', + }, + ], + }, + { + key: 'testExperimentLaunched', + status: 'Launched', + forcedVariations: {}, + audienceIds: [], + layerId: '7', + trafficAllocation: [ + { + entityId: '144448', + endOfRange: 5000, + }, + { + entityId: '144449', + endOfRange: 10000, + }, + ], + id: '144447', + variations: [ + { + key: 'controlLaunched', + id: '144448', + }, + { + key: 'variationLaunched', + id: '144449', + }, + ], + }, + ], + accountId: '12001', + attributes: [ + { + key: 'browser_type', + id: '111094', + }, + { + id: '323434545', + key: 'boolean_key', + }, + { + id: '616727838', + key: 'integer_key', + }, + { + id: '808797686', + key: 'double_key', + }, + { + id: '808797687', + key: 'valid_positive_number', + }, + { + id: '808797688', + key: 'valid_negative_number', + }, + { + id: '808797689', + key: 'invalid_number', + }, + { + id: '808797690', + key: 'array', + }, + ], + audiences: [ + { + name: 'Firefox users', + conditions: '["and", ["or", ["or", {"name": "browser_type", "type": "custom_attribute", "value": "firefox"}]]]', + id: '11154', + }, + ], + projectId: '111001', +}; + +var decideConfig = { + version: '4', + sendFlagDecisions: true, + rollouts: [ + { + experiments: [ + { + audienceIds: ['13389130056'], + forcedVariations: {}, + id: '3332020515', + key: '3332020515', + layerId: '3319450668', + status: 'Running', + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '3324490633', + }, + ], + variations: [ + { + featureEnabled: true, + id: '3324490633', + key: '3324490633', + variables: [], + }, + ], + }, + { + audienceIds: ['12208130097'], + forcedVariations: {}, + id: '3332020494', + key: '3332020494', + layerId: '3319450668', + status: 'Running', + trafficAllocation: [ + { + endOfRange: 0, + entityId: '3324490562', + }, + ], + variations: [ + { + featureEnabled: true, + id: '3324490562', + key: '3324490562', + variables: [], + }, + ], + }, + { + status: 'Running', + audienceIds: [], + variations: [ + { + variables: [], + id: '18257766532', + key: '18257766532', + featureEnabled: true, + }, + ], + id: '18322080788', + key: '18322080788', + layerId: '18263344648', + trafficAllocation: [ + { + entityId: '18257766532', + endOfRange: 10000, + }, + ], + forcedVariations: {}, + }, + ], + id: '3319450668', + }, + ], + anonymizeIP: true, + botFiltering: true, + sdkKey: 'ValidProjectConfigV4', + environmentKey: 'production', + projectId: '10431130345', + variables: [], + featureFlags: [ + { + experimentIds: ['10390977673'], + id: '4482920077', + key: 'feature_1', + rolloutId: '3319450668', + variables: [ + { + defaultValue: '42', + id: '2687470095', + key: 'i_42', + type: 'integer', + }, + { + defaultValue: '4.2', + id: '2689280165', + key: 'd_4_2', + type: 'double', + }, + { + defaultValue: 'true', + id: '2689660112', + key: 'b_true', + type: 'boolean', + }, + { + defaultValue: 'foo', + id: '2696150066', + key: 's_foo', + type: 'string', + }, + { + defaultValue: { + value: 1, + }, + id: '2696150067', + key: 'j_1', + type: 'string', + subType: 'json', + }, + { + defaultValue: 'invalid', + id: '2696150068', + key: 'i_1', + type: 'invalid', + subType: '', + }, + ], + }, + { + experimentIds: ['10420810910'], + id: '4482920078', + key: 'feature_2', + rolloutId: '', + variables: [ + { + defaultValue: '42', + id: '2687470095', + key: 'i_42', + type: 'integer', + }, + ], + }, + { + experimentIds: [], + id: '44829230000', + key: 'feature_3', + rolloutId: '', + variables: [], + }, + ], + experiments: [ + { + status: 'Running', + key: 'exp_with_audience', + layerId: '10420273888', + trafficAllocation: [ + { + entityId: '10389729780', + endOfRange: 10000, + }, + ], + audienceIds: ['13389141123'], + variations: [ + { + variables: [], + featureEnabled: true, + id: '10389729780', + key: 'a', + }, + { + variables: [], + id: '10416523121', + key: 'b', + }, + ], + forcedVariations: {}, + id: '10390977673', + }, + { + status: 'Running', + key: 'exp_no_audience', + layerId: '10417730432', + trafficAllocation: [ + { + entityId: '10418551353', + endOfRange: 10000, + }, + ], + audienceIds: [], + variations: [ + { + variables: [], + featureEnabled: true, + id: '10418551353', + key: 'variation_with_traffic', + }, + { + variables: [], + featureEnabled: false, + id: '10418510624', + key: 'variation_no_traffic', + }, + ], + forcedVariations: {}, + id: '10420810910', + }, + ], + audiences: [ + { + id: '13389141123', + conditions: + '["and",["or",["or",{ "match": "exact", "name": "gender", "type": "custom_attribute", "value": "f"}]]]', + name: 'gender', + }, + { + id: '13389130056', + conditions: + '["and",["or",["or",{ "match": "exact","name": "country","type": "custom_attribute","value": "US"}]]]', + name: 'US', + }, + { + id: '12208130097', + conditions: + '["and",["or",["or",{"match": "exact","name": "browser","type": "custom_attribute","value": "safari"}]]]', + name: 'safari', + }, + { + id: 'age_18', + conditions: '["and",["or",["or",{"match": "gt","name": "age","type": "custom_attribute","value": 18}]]]', + name: 'age_18', + }, + { + id: 'invalid_format', + conditions: '[]', + name: 'invalid_format', + }, + { + id: 'invalid_condition', + conditions: '["and",["or",["or",{"match": "gt","name": "age","type": "custom_attribute","value": "US"}]]]', + name: 'invalid_condition', + }, + { + id: 'invalid_type', + conditions: '["and",["or",["or",{"match": "gt","name": "age","type": "invalid","value": 18}]]]', + name: 'invalid_type', + }, + { + id: 'invalid_match', + conditions: '["and",["or",["or",{"match": "invalid","name": "age","type": "custom_attribute","value": 18}]]]', + name: 'invalid_match', + }, + { + id: 'nil_value', + conditions: '["and",["or",["or",{"match": "gt","name": "age","type": "custom_attribute"}]]]', + name: 'nil_value', + }, + { + id: 'invalid_name', + conditions: '["and",["or",["or",{"match": "gt","type": "custom_attribute","value": 18}]]]', + name: 'invalid_name', + }, + ], + groups: [ + { + policy: 'random', + trafficAllocation: [ + { + entityId: '10390965532', + endOfRange: 10000, + }, + ], + experiments: [ + { + status: 'Running', + key: 'group_exp_1', + layerId: '10420222423', + trafficAllocation: [ + { + entityId: '10389752311', + endOfRange: 10000, + }, + ], + audienceIds: [], + variations: [ + { + variables: [], + featureEnabled: false, + id: '10389752311', + key: 'a', + }, + ], + forcedVariations: {}, + id: '10390965532', + }, + { + status: 'Running', + key: 'group_exp_2', + layerId: '10417730432', + trafficAllocation: [ + { + entityId: '10418524243', + endOfRange: 10000, + }, + ], + audienceIds: [], + variations: [ + { + variables: [], + featureEnabled: false, + id: '10418524243', + key: 'a', + }, + ], + forcedVariations: {}, + id: '10420843432', + }, + ], + id: '13142870430', + }, + ], + attributes: [ + { + id: '10401066117', + key: 'gender', + }, + { + id: '10401066170', + key: 'testvar', + }, + ], + accountId: '10367498574', + events: [ + { + experimentIds: ['10420810910'], + id: '10404198134', + key: 'event1', + }, + { + experimentIds: ['10420810910', '10390977673'], + id: '10404198135', + key: 'event_multiple_running_exp_attached', + }, + ], + revision: '241', +}; + +export var getParsedAudiences = [ + { + name: 'Firefox users', + conditions: ['and', ['or', ['or', { name: 'browser_type', type: 'custom_attribute', value: 'firefox' }]]], + id: '11154', + }, +]; + +export var getTestProjectConfig = function() { + return cloneDeep(config); +}; + +export var getTestDecideProjectConfig = function() { + return cloneDeep(decideConfig); +}; + +var configWithFeatures = { + events: [ + { + key: 'item_bought', + id: '594089', + experimentIds: ['594098', '595010', '599028', '599082'], + }, + ], + featureFlags: [ + { + rolloutId: '594030', + key: 'test_feature', + id: '594021', + experimentIds: [], + variables: [ + { + type: 'boolean', + key: 'new_content', + id: '4919852825313280', + defaultValue: 'false', + }, + { + type: 'integer', + key: 'lasers', + id: '5482802778734592', + defaultValue: '400', + }, + { + type: 'double', + key: 'price', + id: '6045752732155904', + defaultValue: '14.99', + }, + { + type: 'string', + key: 'message', + id: '6327227708866560', + defaultValue: 'Hello', + }, + { + type: 'string', + subType: 'json', + key: 'message_info', + id: '8765345281230956', + defaultValue: '{ "count": 1, "message": "Hello" }', + }, + ], + }, + { + rolloutId: '594059', + key: 'test_feature_2', + id: '594050', + experimentIds: [], + variables: [ + { + type: 'double', + key: 'miles_to_the_wall', + id: '5060590313668608', + defaultValue: '30.34', + }, + { + type: 'string', + key: 'motto', + id: '5342065290379264', + defaultValue: 'Winter is coming', + }, + { + type: 'integer', + key: 'soldiers_available', + id: '6186490220511232', + defaultValue: '1000', + }, + { + type: 'boolean', + key: 'is_winter_coming', + id: '6467965197221888', + defaultValue: 'true', + }, + ], + }, + { + rolloutId: '', + key: 'test_feature_for_experiment', + id: '594081', + experimentIds: ['594098'], + variables: [ + { + type: 'integer', + key: 'num_buttons', + id: '4792309476491264', + defaultValue: '10', + }, + { + type: 'boolean', + key: 'is_button_animated', + id: '5073784453201920', + defaultValue: 'false', + }, + { + type: 'string', + key: 'button_txt', + id: '5636734406623232', + defaultValue: 'Buy me', + }, + { + type: 'double', + key: 'button_width', + id: '6199684360044544', + defaultValue: '50.55', + }, + { + type: 'string', + subType: 'json', + key: 'button_info', + id: '1547854156498475', + defaultValue: '{ "num_buttons": 0, "text": "default value"}', + }, + ], + }, + { + rolloutId: '', + key: 'feature_with_group', + id: '595001', + experimentIds: ['595010'], + variables: [], + }, + { + rolloutId: '599055', + key: 'shared_feature', + id: '599011', + experimentIds: ['599028'], + variables: [ + { + type: 'integer', + key: 'lasers', + id: '4937719889264640', + defaultValue: '100', + }, + { + type: 'string', + key: 'message', + id: '6345094772817920', + defaultValue: 'shared', + }, + ], + }, + { + rolloutId: '', + key: 'unused_flag', + id: '599110', + experimentIds: [], + variables: [], + }, + { + rolloutId: '', + key: 'feature_exp_no_traffic', + id: '4482920079', + experimentIds: ['12115595439'], + variables: [], + }, + { + id: '91115', + key: 'test_feature_in_exclusion_group', + experimentIds: ['42222', '42223', '42224'], + rolloutId: '594059', + variables: [], + }, + { + id: '91116', + key: 'test_feature_in_multiple_experiments', + experimentIds: ['111134', '111135', '111136'], + rolloutId: '594059', + variables: [], + }, + ], + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '594096', + }, + { + endOfRange: 10000, + entityId: '594097', + }, + ], + layerId: '594093', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [ + { + id: '4792309476491264', + value: '2', + }, + { + id: '5073784453201920', + value: 'true', + }, + { + id: '5636734406623232', + value: 'Buy me NOW', + }, + { + id: '6199684360044544', + value: '20.25', + }, + { + id: '1547854156498475', + value: '{ "num_buttons": 1, "text": "first variation"}', + }, + ], + }, + { + key: 'control', + id: '594097', + featureEnabled: true, + variables: [ + { + id: '4792309476491264', + value: '10', + }, + { + id: '5073784453201920', + value: 'false', + }, + { + id: '5636734406623232', + value: 'Buy me', + }, + { + id: '6199684360044544', + value: '50.55', + }, + { + id: '1547854156498475', + value: '{ "num_buttons": 2, "text": "second variation"}', + }, + ], + }, + { + key: 'variation2', + id: '594099', + featureEnabled: false, + variables: [ + { + id: '4792309476491264', + value: '40', + }, + { + id: '5073784453201920', + value: 'true', + }, + { + id: '5636734406623232', + value: 'Buy me Later', + }, + { + id: '6199684360044544', + value: '99.99', + }, + { + id: '1547854156498475', + value: '{ "num_buttons": 3, "text": "third variation"}', + }, + ], + }, + ], + status: 'Running', + key: 'testing_my_feature', + id: '594098', + }, + { + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '599026', + }, + { + endOfRange: 10000, + entityId: '599027', + }, + ], + layerId: '599023', + forcedVariations: {}, + audienceIds: ['594017'], + variations: [ + { + key: 'treatment', + id: '599026', + featureEnabled: true, + variables: [ + { + id: '4937719889264640', + value: '100', + }, + { + id: '6345094772817920', + value: 'shared', + }, + ], + }, + { + key: 'control', + id: '599027', + featureEnabled: false, + variables: [ + { + id: '4937719889264640', + value: '100', + }, + { + id: '6345094772817920', + value: 'shared', + }, + ], + }, + ], + status: 'Running', + key: 'test_shared_feature', + id: '599028', + }, + { + key: 'test_experiment3', + status: 'Running', + layerId: '6', + audienceConditions: ['or', '11160'], + audienceIds: ['11160'], + id: '111134', + forcedVariations: {}, + trafficAllocation: [ + { + entityId: '222239', + endOfRange: 2500, + }, + { + entityId: '', + endOfRange: 5000, + }, + { + entityId: '', + endOfRange: 7500, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + variations: [ + { + id: '222239', + key: 'control', + variables: [], + featureEnabled: false, + }, + ], + }, + { + key: 'test_experiment4', + status: 'Running', + layerId: '7', + audienceConditions: ['or', '11160'], + audienceIds: ['11160'], + id: '111135', + forcedVariations: {}, + trafficAllocation: [ + { + entityId: '222240', + endOfRange: 5000, + }, + { + entityId: '', + endOfRange: 7500, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + variations: [ + { + id: '222240', + key: 'control', + variables: [], + featureEnabled: false, + }, + ], + }, + { + key: 'test_experiment5', + status: 'Running', + layerId: '8', + audienceConditions: ['or', '11160'], + audienceIds: ['11160'], + id: '111136', + forcedVariations: {}, + trafficAllocation: [ + { + entityId: '222241', + endOfRange: 7500, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + variations: [ + { + id: '222241', + key: 'control', + variables: [], + featureEnabled: false, + }, + ], + }, + ], + anonymizeIP: true, + botFiltering: true, + sdkKey: 'ValidProjectConfigV4', + environmentKey: 'development', + audiences: [ + { + id: '594017', + name: 'test_audience', + conditions: + '["and", ["or", ["or", {"type": "custom_attribute", "name": "test_attribute", "value": "test_value"}]]]', + }, + { + id: '11160', + name: 'Test attribute users 3', + conditions: + '["and", ["or", ["or", {"match": "exact", "name": "experiment_attr", "type": "custom_attribute", "value": "group_experiment"}]]]', + }, + ], + revision: '35', + groups: [ + { + policy: 'random', + id: '595024', + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '595008', + }, + { + endOfRange: 10000, + entityId: '595009', + }, + ], + layerId: '595005', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'var', + id: '595008', + variables: [], + }, + { + key: 'con', + id: '595009', + variables: [], + }, + ], + status: 'Running', + key: 'exp_with_group', + id: '595010', + }, + { + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '599080', + }, + { + endOfRange: 10000, + entityId: '599081', + }, + ], + layerId: '599077', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'treatment', + id: '599080', + variables: [], + }, + { + key: 'control', + id: '599081', + variables: [], + }, + ], + status: 'Running', + key: 'other_exp_with_grup', + id: '599082', + }, + ], + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '595010', + }, + { + endOfRange: 10000, + entityId: '599082', + }, + ], + }, + { + policy: 'random', + id: '595025', + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '12098126627', + }, + ], + layerId: '595005', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'all_traffic_variation', + id: '12098126627', + variables: [], + }, + { + key: 'no_traffic_variation', + id: '12098126628', + variables: [], + }, + ], + status: 'Running', + key: 'all_traffic_experiment', + id: '12198292375', + }, + { + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '12098126629', + }, + { + endOfRange: 10000, + entityId: '12098126630', + }, + ], + layerId: '12187694826', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'variation_5000', + id: '12098126629', + variables: [], + }, + { + key: 'variation_10000', + id: '12098126630', + variables: [], + }, + ], + status: 'Running', + key: 'no_traffic_experiment', + id: '12115595439', + }, + ], + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '12198292375', + }, + ], + }, + { + id: '19229', + policy: 'random', + experiments: [ + { + id: '42222', + key: 'group_2_exp_1', + status: 'Running', + audienceConditions: ['or', '11160'], + audienceIds: ['11160'], + layerId: '211183', + variations: [ + { + key: 'var_1', + id: '38901', + featureEnabled: false, + }, + ], + forcedVariations: {}, + trafficAllocation: [ + { + entityId: '38901', + endOfRange: 10000, + }, + ], + variationKeyMap: { + var_1: { + key: 'var_1', + id: '38901', + featureEnabled: false, + }, + }, + }, + { + id: '42223', + key: 'group_2_exp_2', + status: 'Running', + audienceConditions: ['or', '11160'], + audienceIds: ['11160'], + layerId: '211184', + variations: [ + { + key: 'var_1', + id: '38905', + featureEnabled: false, + }, + ], + forcedVariations: {}, + trafficAllocation: [ + { + entityId: '38905', + endOfRange: 10000, + }, + ], + }, + { + id: '42224', + key: 'group_2_exp_3', + status: 'Running', + audienceConditions: ['or', '11160'], + audienceIds: ['11160'], + layerId: '211185', + variations: [ + { + key: 'var_1', + id: '38906', + featureEnabled: false, + }, + ], + forcedVariations: {}, + trafficAllocation: [ + { + entityId: '38906', + endOfRange: 10000, + }, + ], + }, + ], + trafficAllocation: [ + { + entityId: '42222', + endOfRange: 2500, + }, + { + entityId: '42223', + endOfRange: 5000, + }, + { + entityId: '42224', + endOfRange: 7500, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + }, + ], + attributes: [ + { + key: 'test_attribute', + id: '594014', + }, + ], + rollouts: [ + { + id: '594030', + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 5000, + entityId: '594032', + }, + ], + layerId: '594030', + forcedVariations: {}, + audienceIds: ['594017'], + variations: [ + { + key: '594032', + id: '594032', + featureEnabled: true, + variables: [ + { + id: '4919852825313280', + value: 'true', + }, + { + id: '5482802778734592', + value: '395', + }, + { + id: '6045752732155904', + value: '4.99', + }, + { + id: '6327227708866560', + value: 'Hello audience', + }, + { + id: '8765345281230956', + value: '{ "count": 2, "message": "Hello audience" }', + }, + ], + }, + ], + status: 'Not started', + key: '594031', + id: '594031', + }, + { + trafficAllocation: [ + { + endOfRange: 0, + entityId: '594038', + }, + ], + layerId: '594030', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: '594038', + id: '594038', + featureEnabled: false, + variables: [ + { + id: '4919852825313280', + value: 'false', + }, + { + id: '5482802778734592', + value: '400', + }, + { + id: '6045752732155904', + value: '14.99', + }, + { + id: '6327227708866560', + value: 'Hello', + }, + { + id: '8765345281230956', + value: '{ "count": 1, "message": "Hello" }', + }, + ], + }, + ], + status: 'Not started', + key: '594037', + id: '594037', + }, + ], + }, + { + id: '594059', + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '594061', + }, + ], + layerId: '594059', + forcedVariations: {}, + audienceIds: ['594017'], + variations: [ + { + key: '594061', + id: '594061', + featureEnabled: true, + variables: [ + { + id: '5060590313668608', + value: '27.34', + }, + { + id: '5342065290379264', + value: 'Winter is NOT coming', + }, + { + id: '6186490220511232', + value: '10003', + }, + { + id: '6467965197221888', + value: 'false', + }, + ], + }, + ], + status: 'Not started', + key: '594060', + id: '594060', + }, + { + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '594067', + }, + ], + layerId: '594059', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: '594067', + id: '594067', + featureEnabled: true, + variables: [ + { + id: '5060590313668608', + value: '30.34', + }, + { + id: '5342065290379264', + value: 'Winter is coming definitely', + }, + { + id: '6186490220511232', + value: '500', + }, + { + id: '6467965197221888', + value: 'true', + }, + ], + }, + ], + status: 'Not started', + key: '594066', + id: '594066', + }, + ], + }, + { + id: '599055', + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '599057', + }, + ], + layerId: '599055', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: '599057', + id: '599057', + featureEnabled: true, + variables: [ + { + id: '4937719889264640', + value: '200', + }, + { + id: '6345094772817920', + value: "i'm a rollout", + }, + ], + }, + ], + status: 'Not started', + key: '599056', + id: '599056', + }, + ], + }, + ], + projectId: '594001', + accountId: '572018', + version: '4', + variables: [], +}; + +export var getTestProjectConfigWithFeatures = function() { + return cloneDeep(configWithFeatures); +}; + +export var datafileWithFeaturesExpectedData = { + rolloutIdMap: { + 599055: { + id: '599055', + experiments: [ + { + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '599057', + }, + ], + layerId: '599055', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: '599057', + id: '599057', + featureEnabled: true, + variables: [ + { + id: '4937719889264640', + value: '200', + }, + { + id: '6345094772817920', + value: "i'm a rollout", + }, + ], + }, + ], + status: 'Not started', + key: '599056', + id: '599056', + isRollout: true, + variationKeyMap: { + 599057: { + key: '599057', + id: '599057', + featureEnabled: true, + variables: [ + { + id: '4937719889264640', + value: '200', + }, + { + id: '6345094772817920', + value: "i'm a rollout", + }, + ], + }, + }, + }, + ], + }, + 594030: { + experiments: [ + { + audienceIds: ['594017'], + status: 'Not started', + layerId: '594030', + forcedVariations: {}, + variations: [ + { + variables: [ + { + value: 'true', + id: '4919852825313280', + }, + { + value: '395', + id: '5482802778734592', + }, + { + value: '4.99', + id: '6045752732155904', + }, + { + value: 'Hello audience', + id: '6327227708866560', + }, + { + id: '8765345281230956', + value: '{ "count": 2, "message": "Hello audience" }', + }, + ], + featureEnabled: true, + key: '594032', + id: '594032', + }, + ], + trafficAllocation: [ + { + entityId: '594032', + endOfRange: 5000, + }, + ], + key: '594031', + id: '594031', + isRollout: true, + variationKeyMap: { + 594032: { + variables: [ + { + value: 'true', + id: '4919852825313280', + }, + { + value: '395', + id: '5482802778734592', + }, + { + value: '4.99', + id: '6045752732155904', + }, + { + value: 'Hello audience', + id: '6327227708866560', + }, + { + id: '8765345281230956', + value: '{ "count": 2, "message": "Hello audience" }', + }, + ], + featureEnabled: true, + key: '594032', + id: '594032', + }, + }, + }, + { + audienceIds: [], + status: 'Not started', + layerId: '594030', + forcedVariations: {}, + variations: [ + { + variables: [ + { + value: 'false', + id: '4919852825313280', + }, + { + value: '400', + id: '5482802778734592', + }, + { + value: '14.99', + id: '6045752732155904', + }, + { + value: 'Hello', + id: '6327227708866560', + }, + { + id: '8765345281230956', + value: '{ "count": 1, "message": "Hello" }', + }, + ], + featureEnabled: false, + key: '594038', + id: '594038', + }, + ], + trafficAllocation: [ + { + entityId: '594038', + endOfRange: 0, + }, + ], + key: '594037', + id: '594037', + isRollout: true, + variationKeyMap: { + 594038: { + variables: [ + { + value: 'false', + id: '4919852825313280', + }, + { + value: '400', + id: '5482802778734592', + }, + { + value: '14.99', + id: '6045752732155904', + }, + { + value: 'Hello', + id: '6327227708866560', + }, + { + id: '8765345281230956', + value: '{ "count": 1, "message": "Hello" }', + }, + ], + featureEnabled: false, + key: '594038', + id: '594038', + }, + }, + }, + ], + id: '594030', + }, + 594059: { + experiments: [ + { + audienceIds: ['594017'], + status: 'Not started', + layerId: '594059', + forcedVariations: {}, + variations: [ + { + variables: [ + { + value: '27.34', + id: '5060590313668608', + }, + { + value: 'Winter is NOT coming', + id: '5342065290379264', + }, + { + value: '10003', + id: '6186490220511232', + }, + { + value: 'false', + id: '6467965197221888', + }, + ], + featureEnabled: true, + key: '594061', + id: '594061', + }, + ], + trafficAllocation: [ + { + entityId: '594061', + endOfRange: 10000, + }, + ], + key: '594060', + id: '594060', + isRollout: true, + variationKeyMap: { + 594061: { + variables: [ + { + value: '27.34', + id: '5060590313668608', + }, + { + value: 'Winter is NOT coming', + id: '5342065290379264', + }, + { + value: '10003', + id: '6186490220511232', + }, + { + value: 'false', + id: '6467965197221888', + }, + ], + featureEnabled: true, + key: '594061', + id: '594061', + }, + }, + }, + { + audienceIds: [], + status: 'Not started', + layerId: '594059', + forcedVariations: {}, + variations: [ + { + variables: [ + { + value: '30.34', + id: '5060590313668608', + }, + { + value: 'Winter is coming definitely', + id: '5342065290379264', + }, + { + value: '500', + id: '6186490220511232', + }, + { + value: 'true', + id: '6467965197221888', + }, + ], + featureEnabled: true, + key: '594067', + id: '594067', + }, + ], + trafficAllocation: [ + { + entityId: '594067', + endOfRange: 10000, + }, + ], + key: '594066', + id: '594066', + isRollout: true, + variationKeyMap: { + 594067: { + variables: [ + { + value: '30.34', + id: '5060590313668608', + }, + { + value: 'Winter is coming definitely', + id: '5342065290379264', + }, + { + value: '500', + id: '6186490220511232', + }, + { + value: 'true', + id: '6467965197221888', + }, + ], + featureEnabled: true, + key: '594067', + id: '594067', + }, + }, + }, + ], + id: '594059', + }, + }, + + variationVariableUsageMap: { + 222239: {}, + 222240: {}, + 222241: {}, + 594032: { + 4919852825313280: { + id: '4919852825313280', + value: 'true', + }, + 5482802778734592: { + id: '5482802778734592', + value: '395', + }, + 6045752732155904: { + id: '6045752732155904', + value: '4.99', + }, + 6327227708866560: { + id: '6327227708866560', + value: 'Hello audience', + }, + 8765345281230956: { + id: '8765345281230956', + value: '{ "count": 2, "message": "Hello audience" }', + }, + }, + 594038: { + 4919852825313280: { + id: '4919852825313280', + value: 'false', + }, + 5482802778734592: { + id: '5482802778734592', + value: '400', + }, + 6045752732155904: { + id: '6045752732155904', + value: '14.99', + }, + 6327227708866560: { + id: '6327227708866560', + value: 'Hello', + }, + 8765345281230956: { + id: '8765345281230956', + value: '{ "count": 1, "message": "Hello" }', + }, + }, + 594061: { + 5060590313668608: { + id: '5060590313668608', + value: '27.34', + }, + 5342065290379264: { + id: '5342065290379264', + value: 'Winter is NOT coming', + }, + 6186490220511232: { + id: '6186490220511232', + value: '10003', + }, + 6467965197221888: { + id: '6467965197221888', + value: 'false', + }, + }, + 594067: { + 5060590313668608: { + id: '5060590313668608', + value: '30.34', + }, + 5342065290379264: { + id: '5342065290379264', + value: 'Winter is coming definitely', + }, + 6186490220511232: { + id: '6186490220511232', + value: '500', + }, + 6467965197221888: { + id: '6467965197221888', + value: 'true', + }, + }, + 594096: { + 4792309476491264: { + value: '2', + id: '4792309476491264', + }, + 5073784453201920: { + value: 'true', + id: '5073784453201920', + }, + 5636734406623232: { + value: 'Buy me NOW', + id: '5636734406623232', + }, + 6199684360044544: { + value: '20.25', + id: '6199684360044544', + }, + 1547854156498475: { + id: '1547854156498475', + value: '{ "num_buttons": 1, "text": "first variation"}', + }, + }, + 594097: { + 4792309476491264: { + value: '10', + id: '4792309476491264', + }, + 5073784453201920: { + value: 'false', + id: '5073784453201920', + }, + 5636734406623232: { + value: 'Buy me', + id: '5636734406623232', + }, + 6199684360044544: { + value: '50.55', + id: '6199684360044544', + }, + 1547854156498475: { + id: '1547854156498475', + value: '{ "num_buttons": 2, "text": "second variation"}', + }, + }, + 594099: { + 4792309476491264: { + value: '40', + id: '4792309476491264', + }, + 5073784453201920: { + value: 'true', + id: '5073784453201920', + }, + 5636734406623232: { + value: 'Buy me Later', + id: '5636734406623232', + }, + 6199684360044544: { + value: '99.99', + id: '6199684360044544', + }, + 1547854156498475: { + id: '1547854156498475', + value: '{ "num_buttons": 3, "text": "third variation"}', + }, + }, + 595008: {}, + 595009: {}, + 599026: { + 4937719889264640: { + id: '4937719889264640', + value: '100', + }, + 6345094772817920: { + id: '6345094772817920', + value: 'shared', + }, + }, + 599027: { + 4937719889264640: { + id: '4937719889264640', + value: '100', + }, + 6345094772817920: { + id: '6345094772817920', + value: 'shared', + }, + }, + 599057: { + 4937719889264640: { + id: '4937719889264640', + value: '200', + }, + 6345094772817920: { + id: '6345094772817920', + value: "i'm a rollout", + }, + }, + 599080: {}, + 599081: {}, + 12098126627: {}, + 12098126628: {}, + 12098126629: {}, + 12098126630: {}, + }, + + featureKeyMap: { + test_feature: { + variables: [ + { + defaultValue: 'false', + key: 'new_content', + type: 'boolean', + id: '4919852825313280', + }, + { + defaultValue: '400', + key: 'lasers', + type: 'integer', + id: '5482802778734592', + }, + { + defaultValue: '14.99', + key: 'price', + type: 'double', + id: '6045752732155904', + }, + { + defaultValue: 'Hello', + key: 'message', + type: 'string', + id: '6327227708866560', + }, + { + type: 'json', + key: 'message_info', + id: '8765345281230956', + defaultValue: '{ "count": 1, "message": "Hello" }', + }, + ], + experimentIds: [], + rolloutId: '594030', + key: 'test_feature', + id: '594021', + variableKeyMap: { + new_content: { + defaultValue: 'false', + key: 'new_content', + type: 'boolean', + id: '4919852825313280', + }, + lasers: { + defaultValue: '400', + key: 'lasers', + type: 'integer', + id: '5482802778734592', + }, + price: { + defaultValue: '14.99', + key: 'price', + type: 'double', + id: '6045752732155904', + }, + message: { + defaultValue: 'Hello', + key: 'message', + type: 'string', + id: '6327227708866560', + }, + message_info: { + type: 'json', + key: 'message_info', + id: '8765345281230956', + defaultValue: '{ "count": 1, "message": "Hello" }', + }, + }, + }, + test_feature_2: { + variables: [ + { + defaultValue: '30.34', + key: 'miles_to_the_wall', + type: 'double', + id: '5060590313668608', + }, + { + defaultValue: 'Winter is coming', + key: 'motto', + type: 'string', + id: '5342065290379264', + }, + { + defaultValue: '1000', + key: 'soldiers_available', + type: 'integer', + id: '6186490220511232', + }, + { + defaultValue: 'true', + key: 'is_winter_coming', + type: 'boolean', + id: '6467965197221888', + }, + ], + experimentIds: [], + rolloutId: '594059', + key: 'test_feature_2', + id: '594050', + variableKeyMap: { + miles_to_the_wall: { + defaultValue: '30.34', + key: 'miles_to_the_wall', + type: 'double', + id: '5060590313668608', + }, + motto: { + defaultValue: 'Winter is coming', + key: 'motto', + type: 'string', + id: '5342065290379264', + }, + soldiers_available: { + defaultValue: '1000', + key: 'soldiers_available', + type: 'integer', + id: '6186490220511232', + }, + is_winter_coming: { + defaultValue: 'true', + key: 'is_winter_coming', + type: 'boolean', + id: '6467965197221888', + }, + }, + }, + test_feature_for_experiment: { + variables: [ + { + defaultValue: '10', + key: 'num_buttons', + type: 'integer', + id: '4792309476491264', + }, + { + defaultValue: 'false', + key: 'is_button_animated', + type: 'boolean', + id: '5073784453201920', + }, + { + defaultValue: 'Buy me', + key: 'button_txt', + type: 'string', + id: '5636734406623232', + }, + { + defaultValue: '50.55', + key: 'button_width', + type: 'double', + id: '6199684360044544', + }, + { + type: 'json', + key: 'button_info', + id: '1547854156498475', + defaultValue: '{ "num_buttons": 0, "text": "default value"}', + }, + ], + experimentIds: ['594098'], + rolloutId: '', + key: 'test_feature_for_experiment', + id: '594081', + variableKeyMap: { + num_buttons: { + defaultValue: '10', + key: 'num_buttons', + type: 'integer', + id: '4792309476491264', + }, + is_button_animated: { + defaultValue: 'false', + key: 'is_button_animated', + type: 'boolean', + id: '5073784453201920', + }, + button_txt: { + defaultValue: 'Buy me', + key: 'button_txt', + type: 'string', + id: '5636734406623232', + }, + button_width: { + defaultValue: '50.55', + key: 'button_width', + type: 'double', + id: '6199684360044544', + }, + button_info: { + defaultValue: '{ "num_buttons": 0, "text": "default value"}', + id: '1547854156498475', + key: 'button_info', + type: 'json', + }, + }, + }, + // This feature should have a groupId assigned because its experiment is in a group + feature_with_group: { + variables: [], + rolloutId: '', + experimentIds: ['595010'], + key: 'feature_with_group', + id: '595001', + variableKeyMap: {}, + }, + shared_feature: { + rolloutId: '599055', + key: 'shared_feature', + id: '599011', + experimentIds: ['599028'], + variables: [ + { + type: 'integer', + key: 'lasers', + id: '4937719889264640', + defaultValue: '100', + }, + { + type: 'string', + key: 'message', + id: '6345094772817920', + defaultValue: 'shared', + }, + ], + variableKeyMap: { + message: { + type: 'string', + key: 'message', + id: '6345094772817920', + defaultValue: 'shared', + }, + lasers: { + type: 'integer', + key: 'lasers', + id: '4937719889264640', + defaultValue: '100', + }, + }, + }, + unused_flag: { + rolloutId: '', + key: 'unused_flag', + id: '599110', + experimentIds: [], + variables: [], + variableKeyMap: {}, + }, + feature_exp_no_traffic: { + rolloutId: '', + key: 'feature_exp_no_traffic', + id: '4482920079', + experimentIds: ['12115595439'], + variables: [], + variableKeyMap: {}, + }, + test_feature_in_exclusion_group: { + experimentIds: ['42222', '42223', '42224'], + id: '91115', + key: 'test_feature_in_exclusion_group', + rolloutId: '594059', + variableKeyMap: {}, + variables: [], + }, + test_feature_in_multiple_experiments: { + experimentIds: ['111134', '111135', '111136'], + id: '91116', + key: 'test_feature_in_multiple_experiments', + rolloutId: '594059', + variableKeyMap: {}, + variables: [], + }, + }, +}; + +var unsupportedVersionConfig = { + revision: '42', + version: '5', + events: [ + { + key: 'testEvent', + experimentIds: ['111127'], + id: '111095', + }, + { + key: 'Total Revenue', + experimentIds: ['111127'], + id: '111096', + }, + { + key: 'testEventWithAudiences', + experimentIds: ['122227'], + id: '111097', + }, + { + key: 'testEventWithoutExperiments', + experimentIds: [], + id: '111098', + }, + { + key: 'testEventWithExperimentNotRunning', + experimentIds: ['133337'], + id: '111099', + }, + { + key: 'testEventWithMultipleExperiments', + experimentIds: ['111127', '122227', '133337'], + id: '111100', + }, + { + key: 'testEventLaunched', + experimentIds: ['144447'], + id: '111101', + }, + ], + groups: [ + { + id: '666', + policy: 'random', + trafficAllocation: [ + { + entityId: '442', + endOfRange: 3000, + }, + { + entityId: '443', + endOfRange: 6000, + }, + ], + experiments: [ + { + id: '442', + key: 'groupExperiment1', + status: 'Running', + variations: [ + { + id: '551', + key: 'var1exp1', + }, + { + id: '552', + key: 'var2exp1', + }, + ], + trafficAllocation: [ + { + entityId: '551', + endOfRange: 5000, + }, + { + entityId: '552', + endOfRange: 9000, + }, + { + entityId: '', + endOfRange: 10000, + }, + ], + audienceIds: ['11154'], + forcedVariations: {}, + layerId: '1', + }, + { + id: '443', + key: 'groupExperiment2', + status: 'Running', + variations: [ + { + id: '661', + key: 'var1exp2', + }, + { + id: '662', + key: 'var2exp2', + }, + ], + trafficAllocation: [ + { + entityId: '661', + endOfRange: 5000, + }, + { + entityId: '662', + endOfRange: 10000, + }, + ], + audienceIds: [], + forcedVariations: {}, + layerId: '2', + }, + ], + }, + { + id: '667', + policy: 'overlapping', + trafficAllocation: [], + experiments: [ + { + id: '444', + key: 'overlappingGroupExperiment1', + status: 'Running', + variations: [ + { + id: '553', + key: 'overlappingvar1', + }, + { + id: '554', + key: 'overlappingvar2', + }, + ], + trafficAllocation: [ + { + entityId: '553', + endOfRange: 1500, + }, + { + entityId: '554', + endOfRange: 3000, + }, + ], + audienceIds: [], + forcedVariations: {}, + layerId: '3', + }, + ], + }, + ], + experiments: [ + { + key: 'testExperiment', + status: 'Running', + forcedVariations: { + user1: 'control', + user2: 'variation', + }, + audienceIds: [], + layerId: '4', + trafficAllocation: [ + { + entityId: '111128', + endOfRange: 4000, + }, + { + entityId: '111129', + endOfRange: 9000, + }, + ], + id: '111127', + variations: [ + { + key: 'control', + id: '111128', + }, + { + key: 'variation', + id: '111129', + }, + ], + }, + { + key: 'testExperimentWithAudiences', + status: 'Running', + forcedVariations: { + user1: 'controlWithAudience', + user2: 'variationWithAudience', + }, + audienceIds: ['11154'], + layerId: '5', + trafficAllocation: [ + { + entityId: '122228', + endOfRange: 4000, + }, + { + entityId: '122229', + endOfRange: 10000, + }, + ], + id: '122227', + variations: [ + { + key: 'controlWithAudience', + id: '122228', + }, + { + key: 'variationWithAudience', + id: '122229', + }, + ], + }, + { + key: 'testExperimentNotRunning', + status: 'Not started', + forcedVariations: { + user1: 'controlNotRunning', + user2: 'variationNotRunning', + }, + audienceIds: [], + layerId: '6', + trafficAllocation: [ + { + entityId: '133338', + endOfRange: 4000, + }, + { + entityId: '133339', + endOfRange: 10000, + }, + ], + id: '133337', + variations: [ + { + key: 'controlNotRunning', + id: '133338', + }, + { + key: 'variationNotRunning', + id: '133339', + }, + ], + }, + { + key: 'testExperimentLaunched', + status: 'Launched', + forcedVariations: {}, + audienceIds: [], + layerId: '7', + trafficAllocation: [ + { + entityId: '144448', + endOfRange: 5000, + }, + { + entityId: '144449', + endOfRange: 10000, + }, + ], + id: '144447', + variations: [ + { + key: 'controlLaunched', + id: '144448', + }, + { + key: 'variationLaunched', + id: '144449', + }, + ], + }, + ], + accountId: '12001', + attributes: [ + { + key: 'browser_type', + id: '111094', + }, + ], + audiences: [ + { + name: 'Firefox users', + conditions: '["and", ["or", ["or", {"name": "browser_type", "type": "custom_attribute", "value": "firefox"}]]]', + id: '11154', + }, + ], + projectId: '111001', +}; + +export var getUnsupportedVersionConfig = function() { + return cloneDeep(unsupportedVersionConfig); +}; + +var typedAudiencesConfig = { + version: '4', + rollouts: [ + { + experiments: [ + { + status: 'Running', + key: '11488548027', + layerId: '11551226731', + trafficAllocation: [ + { + entityId: '11557362669', + endOfRange: 10000, + }, + ], + audienceIds: [ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ], + variations: [ + { + variables: [], + id: '11557362669', + key: '11557362669', + featureEnabled: true, + }, + ], + forcedVariations: {}, + id: '11488548027', + }, + ], + id: '11551226731', + }, + { + experiments: [ + { + status: 'Paused', + key: '11630490911', + layerId: '11638870867', + trafficAllocation: [ + { + entityId: '11475708558', + endOfRange: 0, + }, + ], + audienceIds: [], + variations: [ + { + variables: [], + id: '11475708558', + key: '11475708558', + featureEnabled: false, + }, + ], + forcedVariations: {}, + id: '11630490911', + }, + ], + id: '11638870867', + }, + { + experiments: [ + { + status: 'Running', + key: '11488548028', + layerId: '11551226732', + trafficAllocation: [ + { + entityId: '11557362670', + endOfRange: 10000, + }, + ], + audienceIds: ['0'], + audienceConditions: [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ], + variations: [ + { + variables: [], + id: '11557362670', + key: '11557362670', + featureEnabled: true, + }, + ], + forcedVariations: {}, + id: '11488548028', + }, + ], + id: '11551226732', + }, + { + experiments: [ + { + status: 'Paused', + key: '11630490912', + layerId: '11638870868', + trafficAllocation: [ + { + entityId: '11475708559', + endOfRange: 0, + }, + ], + audienceIds: [], + variations: [ + { + variables: [], + id: '11475708559', + key: '11475708559', + featureEnabled: false, + }, + ], + forcedVariations: {}, + id: '11630490912', + }, + ], + id: '11638870868', + }, + ], + anonymizeIP: false, + projectId: '11624721371', + variables: [], + featureFlags: [ + { + experimentIds: [], + rolloutId: '11551226731', + variables: [], + id: '11477755619', + key: 'feat', + }, + { + experimentIds: ['11564051718'], + rolloutId: '11638870867', + variables: [ + { + defaultValue: 'x', + type: 'string', + id: '11535264366', + key: 'x', + }, + ], + id: '11567102051', + key: 'feat_with_var', + }, + { + experimentIds: [], + rolloutId: '11551226732', + variables: [], + id: '11567102052', + key: 'feat2', + }, + { + experimentIds: ['1323241599'], + rolloutId: '11638870868', + variables: [ + { + defaultValue: '10', + type: 'integer', + id: '11535264367', + key: 'z', + }, + ], + id: '11567102053', + key: 'feat2_with_var', + }, + ], + experiments: [ + { + status: 'Running', + key: 'feat_with_var_test', + layerId: '11504144555', + trafficAllocation: [ + { + entityId: '11617170975', + endOfRange: 10000, + }, + ], + audienceIds: ['3468206642', '3988293898', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + variations: [ + { + variables: [ + { + id: '11535264366', + value: 'xyz', + }, + ], + id: '11617170975', + key: 'variation_2', + featureEnabled: true, + }, + ], + forcedVariations: {}, + id: '11564051718', + }, + { + id: '1323241597', + key: 'typed_audience_experiment', + layerId: '1630555627', + status: 'Running', + variations: [ + { + id: '1423767503', + key: 'A', + variables: [], + }, + ], + trafficAllocation: [ + { + entityId: '1423767503', + endOfRange: 10000, + }, + ], + audienceIds: ['3468206642', '3988293898', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + forcedVariations: {}, + }, + { + id: '1323241598', + key: 'audience_combinations_experiment', + layerId: '1323241598', + status: 'Running', + variations: [ + { + id: '1423767504', + key: 'A', + variables: [], + }, + ], + trafficAllocation: [ + { + entityId: '1423767504', + endOfRange: 10000, + }, + ], + audienceIds: ['0'], + audienceConditions: [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ], + forcedVariations: {}, + }, + { + id: '1323241599', + key: 'feat2_with_var_test', + layerId: '1323241600', + status: 'Running', + variations: [ + { + variables: [ + { + id: '11535264367', + value: '150', + }, + ], + id: '1423767505', + key: 'variation_2', + featureEnabled: true, + }, + ], + trafficAllocation: [ + { + entityId: '1423767505', + endOfRange: 10000, + }, + ], + audienceIds: ['0'], + audienceConditions: [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ], + forcedVariations: {}, + }, + ], + audiences: [ + { + id: '3468206642', + name: 'exactString', + conditions: '["and", ["or", ["or", {"name": "house", "type": "custom_attribute", "value": "Gryffindor"}]]]', + }, + { + id: '3988293898', + name: '$$dummySubstringString', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + id: '3988293899', + name: '$$dummyExists', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + id: '3468206646', + name: '$$dummyExactNumber', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + id: '3468206647', + name: '$$dummyGtNumber', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + id: '3468206644', + name: '$$dummyLtNumber', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + id: '3468206643', + name: '$$dummyExactBoolean', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + id: '0', + name: '$$dummy', + conditions: '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + ], + typedAudiences: [ + { + id: '3988293898', + name: 'substringString', + conditions: [ + 'and', + ['or', ['or', { name: 'house', type: 'custom_attribute', match: 'substring', value: 'Slytherin' }]], + ], + }, + { + id: '3988293899', + name: 'exists', + conditions: ['and', ['or', ['or', { name: 'favorite_ice_cream', type: 'custom_attribute', match: 'exists' }]]], + }, + { + id: '3468206646', + name: 'exactNumber', + conditions: ['and', ['or', ['or', { name: 'lasers', type: 'custom_attribute', match: 'exact', value: 45.5 }]]], + }, + { + id: '3468206647', + name: 'gtNumber', + conditions: ['and', ['or', ['or', { name: 'lasers', type: 'custom_attribute', match: 'gt', value: 70 }]]], + }, + { + id: '3468206644', + name: 'ltNumber', + conditions: ['and', ['or', ['or', { name: 'lasers', type: 'custom_attribute', match: 'lt', value: 1.0 }]]], + }, + { + id: '3468206643', + name: 'exactBoolean', + conditions: [ + 'and', + ['or', ['or', { name: 'should_do_it', type: 'custom_attribute', match: 'exact', value: true }]], + ], + }, + ], + groups: [], + attributes: [ + { + key: 'house', + id: '594015', + }, + { + key: 'lasers', + id: '594016', + }, + { + key: 'should_do_it', + id: '594017', + }, + { + key: 'favorite_ice_cream', + id: '594018', + }, + ], + botFiltering: false, + accountId: '4879520872', + events: [ + { + key: 'item_bought', + id: '594089', + experimentIds: ['11564051718', '1323241597'], + }, + { + key: 'user_signed_up', + id: '594090', + experimentIds: ['1323241598', '1323241599'], + }, + ], + revision: '3', +}; + +export var getTypedAudiencesConfig = function() { + return cloneDeep(typedAudiencesConfig); +}; + +var odpIntegratedConfigWithSegments = { + version: '4', + sendFlagDecisions: true, + rollouts: [ + { + experiments: [ + { + audienceIds: ['13389130056'], + forcedVariations: {}, + id: '3332020515', + key: 'rollout-rule-1', + layerId: '3319450668', + status: 'Running', + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '3324490633', + }, + ], + variations: [ + { + featureEnabled: true, + id: '3324490633', + key: 'rollout-variation-on', + variables: [], + }, + ], + }, + { + audienceIds: [], + forcedVariations: {}, + id: '3332020556', + key: 'rollout-rule-2', + layerId: '3319450668', + status: 'Running', + trafficAllocation: [ + { + endOfRange: 10000, + entityId: '3324490644', + }, + ], + variations: [ + { + featureEnabled: false, + id: '3324490644', + key: 'rollout-variation-off', + variables: [], + }, + ], + }, + ], + id: '3319450668', + }, + ], + anonymizeIP: true, + botFiltering: true, + projectId: '10431130345', + variables: [], + featureFlags: [ + { + experimentIds: ['10390977673'], + id: '4482920077', + key: 'flag-segment', + rolloutId: '3319450668', + variables: [ + { + defaultValue: '42', + id: '2687470095', + key: 'i_42', + type: 'integer', + }, + ], + }, + ], + experiments: [ + { + status: 'Running', + key: 'experiment-segment', + layerId: '10420273888', + trafficAllocation: [ + { + entityId: '10389729780', + endOfRange: 10000, + }, + ], + audienceIds: ['$opt_dummy_audience'], + audienceConditions: ['or', '13389142234', '13389141123'], + variations: [ + { + variables: [], + featureEnabled: true, + id: '10389729780', + key: 'variation-a', + }, + { + variables: [], + id: '10416523121', + key: 'variation-b', + }, + ], + forcedVariations: {}, + id: '10390977673', + }, + ], + groups: [], + integrations: [ + { + key: 'odp', + host: 'https://api.zaius.com', + publicKey: 'W4WzcEs-ABgXorzY7h1LCQ', + pixelUrl: 'https://jumbe.zaius.com', + }, + { + key: 'odp', + host: 'https://api.zzzzaius.com', + publicKey: 'W4WzcEs-ABgXorzssssY7h1LCQ', + pixelUrl: 'https://jumbe.zzzzaius.com', + }, + { + key: 'odp', + a: '1', + b: '2', + }, + { + key: 'x', + test: 'foobar', + }, + ], + typedAudiences: [ + { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-1', + }, + { + id: '13389142234', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-1', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-1', + }, + { + id: '13389130056', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: 'odp-segment-2', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + { + value: 'us', + type: 'custom_attribute', + name: 'country', + match: 'exact', + }, + ], + [ + 'or', + { + value: 'odp-segment-3', + type: 'third_party_dimension', + name: 'odp.audiences', + match: 'qualified', + }, + ], + ], + ], + name: 'odp-segment-2', + }, + ], + audiences: [ + { + id: '13389141123', + conditions: '["and", ["or", ["or", {"match": "gt", "name": "age", "type": "custom_attribute", "value": 20}]]]', + name: 'adult', + }, + ], + attributes: [ + { + id: '10401066117', + key: 'gender', + }, + { + id: '10401066170', + key: 'testvar', + }, + ], + accountId: '10367498574', + events: [], + revision: '101', +}; + +export var getOdpIntegratedConfigWithSegments = function() { + return cloneDeep(odpIntegratedConfigWithSegments); +}; + +var odpIntegratedConfigWithoutSegments = { + version: '4', + rollouts: [], + anonymizeIP: true, + projectId: '10431130345', + variables: [], + featureFlags: [], + experiments: [], + audiences: [], + groups: [], + attributes: [], + accountId: '10367498574', + events: [], + integrations: [ + { + key: 'odp', + host: 'https://api.zaius.com', + publicKey: 'W4WzcEs-ABgXorzY7h1LCQ', + pixelUrl: 'https://jumbe.zaius.com', + }, + { + key: 'odp', + a: '1', + b: '2', + }, + { + key: 'x', + test: 'foobar', + }, + ], + revision: '100', +}; + +export var getOdpIntegratedConfigWithoutSegments = function() { + return cloneDeep(odpIntegratedConfigWithoutSegments); +}; + +var odpIntegratedConfigWithoutKey = { + version: '4', + rollouts: [], + anonymizeIP: true, + projectId: '10431130345', + variables: [], + featureFlags: [], + experiments: [], + audiences: [], + groups: [], + attributes: [], + accountId: '10367498574', + events: [], + integrations: [ + { + host: 'https://api.zaius.com', + publicKey: 'W4WzcEs-ABgXorzY7h1LCQ', + pixelUrl: 'https://jumbe.zaius.com', + }, + ], + revision: '100', +}; + +export var getOdpIntegratedConfigWithoutKey = function() { + return cloneDeep(odpIntegratedConfigWithoutKey); +}; + +export var typedAudiencesById = { + 3468206642: { + id: '3468206642', + name: 'exactString', + conditions: ['and', ['or', ['or', { name: 'house', type: 'custom_attribute', value: 'Gryffindor' }]]], + }, + 3988293898: { + id: '3988293898', + name: 'substringString', + conditions: [ + 'and', + ['or', ['or', { name: 'house', type: 'custom_attribute', match: 'substring', value: 'Slytherin' }]], + ], + }, + 3988293899: { + id: '3988293899', + name: 'exists', + conditions: ['and', ['or', ['or', { name: 'favorite_ice_cream', type: 'custom_attribute', match: 'exists' }]]], + }, + 3468206646: { + id: '3468206646', + name: 'exactNumber', + conditions: ['and', ['or', ['or', { name: 'lasers', type: 'custom_attribute', match: 'exact', value: 45.5 }]]], + }, + 3468206647: { + id: '3468206647', + name: 'gtNumber', + conditions: ['and', ['or', ['or', { name: 'lasers', type: 'custom_attribute', match: 'gt', value: 70 }]]], + }, + 3468206644: { + id: '3468206644', + name: 'ltNumber', + conditions: ['and', ['or', ['or', { name: 'lasers', type: 'custom_attribute', match: 'lt', value: 1.0 }]]], + }, + 3468206643: { + id: '3468206643', + name: 'exactBoolean', + conditions: [ + 'and', + ['or', ['or', { name: 'should_do_it', type: 'custom_attribute', match: 'exact', value: true }]], + ], + }, + 0: { + id: '0', + name: '$$dummy', + conditions: { type: 'custom_attribute', name: '$opt_dummy_attribute', value: 'impossible_value' }, + }, +}; + +var mutexFeatureTestsConfig = { + version: '4', + rollouts: [ + { + experiments: [ + { + status: 'Not started', + audienceIds: [], + variations: [{ variables: [], id: '17138530965', key: '17138530965', featureEnabled: false }], + id: '17138130490', + key: '17138130490', + layerId: '17151011617', + trafficAllocation: [{ entityId: '17138530965', endOfRange: 0 }], + forcedVariations: {}, + }, + ], + id: '17151011617', + }, + ], + typedAudiences: [], + anonymizeIP: false, + projectId: '1715448053799999', + variables: [], + featureFlags: [ + { + experimentIds: ['17128410791', '17139931304'], + rolloutId: '17151011617', + variables: [], + id: '17146211047', + key: 'f', + }, + ], + experiments: [], + audiences: [], + groups: [ + { + policy: 'random', + trafficAllocation: [ + { entityId: '17139931304', endOfRange: 9900 }, + { entityId: '17128410791', endOfRange: 10000 }, + ], + experiments: [ + { + status: 'Running', + audienceIds: [], + variations: [ + { variables: [], id: 17155031309, key: 'variation_1', featureEnabled: false }, + { variables: [], id: 17124610952, key: 'variation_2', featureEnabled: true }, + ], + id: '17139931304', + key: 'f_test2', + layerId: '17149391594', + trafficAllocation: [ + { entityId: '17155031309', endOfRange: 5000 }, + { entityId: '17124610952', endOfRange: 10000 }, + ], + forcedVariations: {}, + }, + { + status: 'Running', + audienceIds: [], + variations: [ + { variables: [], id: '17175820099', key: 'variation_1', featureEnabled: false }, + { variables: [], id: '17144050391', key: 'variation_2', featureEnabled: true }, + ], + id: '17128410791', + key: 'f_test1', + layerId: '17145581153', + trafficAllocation: [ + { entityId: '17175820099', endOfRange: 5000 }, + { entityId: '17144050391', endOfRange: 10000 }, + ], + forcedVariations: {}, + }, + ], + id: '17142090293', + }, + ], + attributes: [], + botFiltering: false, + accountId: '4879520872999', + events: [{ experimentIds: ['17128410791', '17139931304'], id: '17140380990', key: 'e' }], + revision: '12', +}; + +export var getMutexFeatureTestsConfig = function() { + return cloneDeep(mutexFeatureTestsConfig); +}; + +export var rolloutDecisionObj = { + experiment: null, + variation: null, + decisionSource: 'rollout' as const, +}; + +export var featureTestDecisionObj = { + experiment: { + trafficAllocation: [ + { endOfRange: 5000, entityId: '594096' }, + { endOfRange: 10000, entityId: '594097' }, + ], + layerId: '594093', + forcedVariations: {}, + audienceIds: [], + variations: [ + { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [], + variablesMap: {}, + }, + { + key: 'control', + id: '594097', + featureEnabled: true, + variables: [], + variablesMap: {} + }, + ], + status: 'Running', + key: 'testing_my_feature', + id: '594098', + variationKeyMap: { + variation: { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [], + variablesMap: {} + }, + control: { + key: 'control', + id: '594097', + featureEnabled: true, + variables: [], + variablesMap: {} + }, + }, + audienceConditions: [] + }, + variation: { + key: 'variation', + id: '594096', + featureEnabled: true, + variables: [], + variablesMap: {} + }, + decisionSource: 'feature-test' as const, +}; + +var similarRuleKeyConfig = { + version: '4', + rollouts: [ + { + experiments: [ + { + status: 'Running', + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: '5452', + key: 'on', + featureEnabled: true, + }, + ], + forcedVariations: {}, + key: 'targeted_delivery', + layerId: '9300000004981', + trafficAllocation: [ + { + entityId: '5452', + endOfRange: 10000, + }, + ], + id: '9300000004981', + }, + { + status: 'Running', + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: '5451', + key: 'off', + featureEnabled: false, + }, + ], + forcedVariations: {}, + key: 'default-rollout-2029-20301771717', + layerId: 'default-layer-rollout-2029-20301771717', + trafficAllocation: [ + { + entityId: '5451', + endOfRange: 10000, + }, + ], + id: 'default-rollout-2029-20301771717', + }, + ], + id: 'rollout-2029-20301771717', + }, + { + experiments: [ + { + status: 'Running', + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: '5450', + key: 'on', + featureEnabled: true, + }, + ], + forcedVariations: {}, + key: 'targeted_delivery', + layerId: '9300000004979', + trafficAllocation: [ + { + entityId: '5450', + endOfRange: 10000, + }, + ], + id: '9300000004979', + }, + { + status: 'Running', + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: '5449', + key: 'off', + featureEnabled: false, + }, + ], + forcedVariations: {}, + key: 'default-rollout-2028-20301771717', + layerId: 'default-layer-rollout-2028-20301771717', + trafficAllocation: [ + { + entityId: '5449', + endOfRange: 10000, + }, + ], + id: 'default-rollout-2028-20301771717', + }, + ], + id: 'rollout-2028-20301771717', + }, + { + experiments: [ + { + status: 'Running', + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: '5448', + key: 'on', + featureEnabled: true, + }, + ], + forcedVariations: {}, + key: 'targeted_delivery', + layerId: '9300000004977', + trafficAllocation: [ + { + entityId: '5448', + endOfRange: 10000, + }, + ], + id: '9300000004977', + }, + { + status: 'Running', + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: '5447', + key: 'off', + featureEnabled: false, + }, + ], + forcedVariations: {}, + key: 'default-rollout-2027-20301771717', + layerId: 'default-layer-rollout-2027-20301771717', + trafficAllocation: [ + { + entityId: '5447', + endOfRange: 10000, + }, + ], + id: 'default-rollout-2027-20301771717', + }, + ], + id: 'rollout-2027-20301771717', + }, + ], + typedAudiences: [], + anonymizeIP: true, + projectId: '20286295225', + variables: [], + featureFlags: [ + { + experimentIds: [], + rolloutId: 'rollout-2029-20301771717', + variables: [], + id: '2029', + key: 'flag_3', + }, + { + experimentIds: [], + rolloutId: 'rollout-2028-20301771717', + variables: [], + id: '2028', + key: 'flag_2', + }, + { + experimentIds: [], + rolloutId: 'rollout-2027-20301771717', + variables: [], + id: '2027', + key: 'flag_1', + }, + ], + experiments: [], + audiences: [ + { + conditions: + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + id: '$opt_dummy_audience', + name: 'Optimizely-Generated Audience for Backwards Compatibility', + }, + ], + groups: [], + attributes: [], + botFiltering: false, + accountId: '19947277778', + events: [], + revision: '11', + sendFlagDecisions: true, +}; + +export var getSimilarRuleKeyConfig = function() { + return cloneDeep(similarRuleKeyConfig); +}; + +var similarExperimentKeysConfig = { + version: '4', + rollouts: [], + typedAudiences: [ + { + id: '20415611520', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: true, + type: 'custom_attribute', + name: 'hiddenLiveEnabled', + match: 'exact', + }, + ], + ], + ], + name: 'test1', + }, + { + id: '20406066925', + conditions: [ + 'and', + [ + 'or', + [ + 'or', + { + value: false, + type: 'custom_attribute', + name: 'hiddenLiveEnabled', + match: 'exact', + }, + ], + ], + ], + name: 'test2', + }, + ], + anonymizeIP: true, + projectId: '20430981610', + variables: [], + featureFlags: [ + { + experimentIds: ['9300000007569'], + rolloutId: '', + variables: [], + id: '3045', + key: 'flag1', + }, + { + experimentIds: ['9300000007573'], + rolloutId: '', + variables: [], + id: '3046', + key: 'flag2', + }, + ], + experiments: [ + { + status: 'Running', + audienceConditions: ['or', '20415611520'], + audienceIds: ['20415611520'], + variations: [ + { + variables: [], + id: '8045', + key: 'variation1', + featureEnabled: true, + }, + ], + forcedVariations: {}, + key: 'targeted_delivery', + layerId: '9300000007569', + trafficAllocation: [ + { + entityId: '8045', + endOfRange: 10000, + }, + ], + id: '9300000007569', + }, + { + status: 'Running', + audienceConditions: ['or', '20406066925'], + audienceIds: ['20406066925'], + variations: [ + { + variables: [], + id: '8048', + key: 'variation2', + featureEnabled: true, + }, + ], + forcedVariations: {}, + key: 'targeted_delivery', + layerId: '9300000007573', + trafficAllocation: [ + { + entityId: '8048', + endOfRange: 10000, + }, + ], + id: '9300000007573', + }, + ], + audiences: [ + { + id: '20415611520', + conditions: + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + name: 'test1', + }, + { + id: '20406066925', + conditions: + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + name: 'test2', + }, + { + conditions: + '["or", {"match": "exact", "name": "$opt_dummy_attribute", "type": "custom_attribute", "value": "$opt_dummy_value"}]', + id: '$opt_dummy_audience', + name: 'Optimizely-Generated Audience for Backwards Compatibility', + }, + ], + groups: [], + attributes: [ + { + id: '20408641883', + key: 'hiddenLiveEnabled', + }, + ], + botFiltering: false, + accountId: '17882702980', + events: [], + revision: '25', + sendFlagDecisions: true, +}; + +export var getSimilarExperimentKeyConfig = function() { + return cloneDeep(similarExperimentKeysConfig); +}; + +const duplicateExperimentKeyConfig = { + "accountId": "23793010390", + "projectId": "24812320344", + "revision": "24", + "attributes": [ + { + "id": "24778491463", + "key": "country" + }, + { + "id": "24802951640", + "key": "likes_donuts" + } + ], + "audiences": [ + { + "name": "mutext_feat", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "24837020039" + }, + { + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]" + } + ], + "version": "4", + "events": [], + "integrations": [], + "anonymizeIP": true, + "botFiltering": false, + "typedAudiences": [ + { + "name": "mutext_feat", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "match": "exact", + "name": "country", + "type": "custom_attribute", + "value": "US" + } + ], + [ + "or", + { + "match": "exact", + "name": "likes_donuts", + "type": "custom_attribute", + "value": true + } + ] + ] + ], + "id": "24837020039" + } + ], + "variables": [], + "environmentKey": "production", + "sdkKey": "BBhivmjEBF1KLK8HkMrvj", + "featureFlags": [ + { + "id": "101043", + "key": "mutext_feat2", + "rolloutId": "rollout-101043-24783691394", + "experimentIds": [ + "9300000361925" + ], + "variables": [] + }, + { + "id": "101044", + "key": "mutex_feat", + "rolloutId": "rollout-101044-24783691394", + "experimentIds": [ + "9300000365056" + ], + "variables": [] + } + ], + "rollouts": [ + { + "id": "rollout-101043-24783691394", + "experiments": [ + { + "id": "default-rollout-101043-24783691394", + "key": "default-rollout-101043-24783691394", + "status": "Running", + "layerId": "rollout-101043-24783691394", + "variations": [ + { + "id": "321340", + "key": "off", + "featureEnabled": false, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "321340", + "endOfRange": 10000 + } + ], + "forcedVariations": {}, + "audienceIds": [], + "audienceConditions": [] + } + ] + }, + { + "id": "rollout-101044-24783691394", + "experiments": [ + { + "id": "default-rollout-101044-24783691394", + "key": "default-rollout-101044-24783691394", + "status": "Running", + "layerId": "rollout-101044-24783691394", + "variations": [ + { + "id": "321343", + "key": "off", + "featureEnabled": false, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "321343", + "endOfRange": 10000 + } + ], + "forcedVariations": {}, + "audienceIds": [], + "audienceConditions": [] + } + ] + } + ], + "experiments": [ + { + "id": "9300000361925", + "key": "experiment_rule", + "status": "Running", + "layerId": "9300000284731", + "variations": [ + { + "id": "321342", + "key": "variation_1", + "featureEnabled": true, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "321342", + "endOfRange": 10000 + } + ], + "forcedVariations": {}, + "audienceIds": [ + "24837020039" + ], + "audienceConditions": [ + "or", + "24837020039" + ] + }, + { + "id": "9300000365056", + "key": "experiment_rule", + "status": "Running", + "layerId": "9300000287826", + "variations": [ + { + "id": "321345", + "key": "variation_1", + "featureEnabled": true, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "321345", + "endOfRange": 10000 + } + ], + "forcedVariations": {}, + "audienceIds": [], + "audienceConditions": [] + } + ], + "groups": [] +}; + +export const getDuplicateExperimentKeyConfig = function() { + return cloneDeep(duplicateExperimentKeyConfig); +}; + +export default { + getTestProjectConfig: getTestProjectConfig, + getTestDecideProjectConfig: getTestDecideProjectConfig, + getParsedAudiences: getParsedAudiences, + getTestProjectConfigWithFeatures: getTestProjectConfigWithFeatures, + datafileWithFeaturesExpectedData: datafileWithFeaturesExpectedData, + getUnsupportedVersionConfig: getUnsupportedVersionConfig, + getTypedAudiencesConfig: getTypedAudiencesConfig, + getOdpIntegratedConfigWithSegments: getOdpIntegratedConfigWithSegments, + getOdpIntegratedConfigWithoutSegments: getOdpIntegratedConfigWithoutSegments, + getOdpIntegratedConfigWithoutKey: getOdpIntegratedConfigWithoutKey, + typedAudiencesById: typedAudiencesById, + getMutexFeatureTestsConfig: getMutexFeatureTestsConfig, + getSimilarRuleKeyConfig: getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig: getSimilarExperimentKeyConfig, + getDuplicateExperimentKeyConfig: getDuplicateExperimentKeyConfig, +}; diff --git a/lib/utils/attributes_validator/index.spec.ts b/lib/utils/attributes_validator/index.spec.ts new file mode 100644 index 000000000..645fa2113 --- /dev/null +++ b/lib/utils/attributes_validator/index.spec.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import * as attributesValidator from './'; +import { INVALID_ATTRIBUTES, UNDEFINED_ATTRIBUTE } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +describe('validate', () => { + it('should validate the given attributes if attributes is an object', () => { + expect(attributesValidator.validate({ testAttribute: 'testValue' })).toBe(true); + }); + + it('should throw an error if attributes is an array', () => { + const attributesArray = ['notGonnaWork']; + + expect(() => attributesValidator.validate(attributesArray)).toThrow(OptimizelyError); + + try { + attributesValidator.validate(attributesArray); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_ATTRIBUTES); + } + }); + + it('should throw an error if attributes is null', () => { + expect(() => attributesValidator.validate(null)).toThrowError(OptimizelyError); + + try { + attributesValidator.validate(null); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_ATTRIBUTES); + } + }); + + it('should throw an error if attributes is a function', () => { + function invalidInput() { + console.log('This is an invalid input!'); + } + + expect(() => attributesValidator.validate(invalidInput)).toThrowError(OptimizelyError); + + try { + attributesValidator.validate(invalidInput); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_ATTRIBUTES); + } + }); + + it('should throw an error if attributes contains a key with an undefined value', () => { + const attributeKey = 'testAttribute'; + const attributes: Record<string, unknown> = {}; + attributes[attributeKey] = undefined; + + expect(() => attributesValidator.validate(attributes)).toThrowError(OptimizelyError); + + try { + attributesValidator.validate(attributes); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(UNDEFINED_ATTRIBUTE); + expect(err.params).toEqual([attributeKey]); + } + }); +}); + +describe('isAttributeValid', () => { + it('isAttributeValid returns true for valid values', () => { + const userAttributes: Record<string, unknown> = { + browser_type: 'Chrome', + is_firefox: false, + num_users: 10, + pi_value: 3.14, + '': 'javascript', + }; + + Object.keys(userAttributes).forEach(key => { + const value = userAttributes[key]; + + expect(attributesValidator.isAttributeValid(key, value)).toBe(true); + }); + }); + it('isAttributeValid returns false for invalid values', () => { + const userAttributes: Record<string, unknown> = { + null: null, + objects: { a: 'b' }, + array: [1, 2, 3], + infinity: Infinity, + negativeInfinity: -Infinity, + NaN: NaN, + }; + + Object.keys(userAttributes).forEach(key => { + const value = userAttributes[key]; + + expect(attributesValidator.isAttributeValid(key, value)).toBe(false); + }); + }); +}); diff --git a/packages/optimizely-sdk/lib/utils/attributes_validator/index.tests.js b/lib/utils/attributes_validator/index.tests.js similarity index 61% rename from packages/optimizely-sdk/lib/utils/attributes_validator/index.tests.js rename to lib/utils/attributes_validator/index.tests.js index 78b808d65..ecfc4bccb 100644 --- a/packages/optimizely-sdk/lib/utils/attributes_validator/index.tests.js +++ b/lib/utils/attributes_validator/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2016, 2018-2019, Optimizely + * Copyright 2016, 2018-2020, 2022, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,82 +13,86 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var chai = require('chai'); -var assert = chai.assert; -var sprintf = require('sprintf-js').sprintf; -var attributesValidator = require('./'); -var fns = require('./../fns/'); +import { assert } from 'chai'; +import { sprintf } from '../../utils/fns'; -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; +import * as attributesValidator from './'; +import { INVALID_ATTRIBUTES, UNDEFINED_ATTRIBUTE } from 'error_message'; describe('lib/utils/attributes_validator', function() { describe('APIs', function() { describe('validate', function() { it('should validate the given attributes if attributes is an object', function() { - assert.isTrue(attributesValidator.validate({testAttribute: 'testValue'})); + assert.isTrue(attributesValidator.validate({ testAttribute: 'testValue' })); }); it('should throw an error if attributes is an array', function() { var attributesArray = ['notGonnaWork']; - assert.throws(function() { + const ex = assert.throws(function() { attributesValidator.validate(attributesArray); - }, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_ATTRIBUTES); }); it('should throw an error if attributes is null', function() { - assert.throws(function() { + const ex = assert.throws(function() { attributesValidator.validate(null); - }, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_ATTRIBUTES); }); it('should throw an error if attributes is a function', function() { function invalidInput() { console.log('This is an invalid input!'); } - assert.throws(function() { + const ex = assert.throws(function() { attributesValidator.validate(invalidInput); - }, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_ATTRIBUTES); }); - it('should throw an error if attributes contains a key with an undefined value', function() { var attributeKey = 'testAttribute'; var attributes = {}; attributes[attributeKey] = undefined; - assert.throws(function() { + const ex = assert.throws(function() { attributesValidator.validate(attributes); - }, sprintf(ERROR_MESSAGES.UNDEFINED_ATTRIBUTE, 'ATTRIBUTES_VALIDATOR', attributeKey)); + }); + assert.equal(ex.baseMessage, UNDEFINED_ATTRIBUTE); + assert.deepEqual(ex.params, [attributeKey]); }); }); describe('isAttributeValid', function() { it('isAttributeValid returns true for valid values', function() { var userAttributes = { - 'browser_type': 'Chrome', - 'is_firefox': false, - 'num_users': 10, - 'pi_value': 3.14, + browser_type: 'Chrome', + is_firefox: false, + num_users: 10, + pi_value: 3.14, '': 'javascript', }; - fns.forOwn(userAttributes, function(value, key) { + Object.keys(userAttributes).forEach(function(key) { + var value = userAttributes[key]; assert.isTrue(attributesValidator.isAttributeValid(key, value)); }); }); it('isAttributeValid returns false for invalid values', function() { var userAttributes = { - 'null': null, - 'objects': {a: 'b'}, - 'array': [1, 2, 3], - 'infinity': Infinity, - 'negativeInfinity': -Infinity, - 'NaN': NaN, - 'outOfBound': Math.pow(2, 53) + 2, + null: null, + objects: { a: 'b' }, + array: [1, 2, 3], + infinity: Infinity, + negativeInfinity: -Infinity, + NaN: NaN, + outOfBound: Math.pow(2, 53) + 2, }; - fns.forOwn(userAttributes, function(value, key) { + Object.keys(userAttributes).forEach(function(key) { + var value = userAttributes[key]; assert.isFalse(attributesValidator.isAttributeValid(key, value)); }); }); diff --git a/lib/utils/attributes_validator/index.ts b/lib/utils/attributes_validator/index.ts new file mode 100644 index 000000000..08b50eb43 --- /dev/null +++ b/lib/utils/attributes_validator/index.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2016, 2018-2020, 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ObjectWithUnknownProperties } from '../../shared_types'; + +import fns from '../../utils/fns'; +import { INVALID_ATTRIBUTES, UNDEFINED_ATTRIBUTE } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +/** + * Validates user's provided attributes + * @param {unknown} attributes + * @return {boolean} true if the attributes are valid + * @throws If the attributes are not valid + */ + +export function validate(attributes: unknown): boolean { + if (typeof attributes === 'object' && !Array.isArray(attributes) && attributes !== null) { + Object.keys(attributes).forEach(function(key) { + if (typeof (attributes as ObjectWithUnknownProperties)[key] === 'undefined') { + throw new OptimizelyError(UNDEFINED_ATTRIBUTE, key); + } + }); + return true; + } else { + throw new OptimizelyError(INVALID_ATTRIBUTES); + } +} + +/** + * Validates user's provided attribute + * @param {unknown} attributeKey + * @param {unknown} attributeValue + * @return {boolean} true if the attribute is valid + */ +export function isAttributeValid(attributeKey: unknown, attributeValue: unknown): boolean { + return ( + typeof attributeKey === 'string' && + (typeof attributeValue === 'string' || + typeof attributeValue === 'boolean' || + (fns.isNumber(attributeValue) && fns.isSafeInteger(attributeValue))) + ); +} diff --git a/lib/utils/cache/async_storage_cache.react_native.spec.ts b/lib/utils/cache/async_storage_cache.react_native.spec.ts new file mode 100644 index 000000000..f67fca7bf --- /dev/null +++ b/lib/utils/cache/async_storage_cache.react_native.spec.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, describe, it, expect } from 'vitest'; +import { AsyncStorageCache } from './async_storage_cache.react_native'; +import { getDefaultAsyncStorage } from '../import.react_native/@react-native-async-storage/async-storage'; + +vi.mock('@react-native-async-storage/async-storage'); + +type TestData = { + a: number; + b: string; + d: { e: boolean }; +}; + +describe('AsyncStorageCache', () => { + const asyncStorage = getDefaultAsyncStorage(); + + it('should store a stringified value in async storage', async () => { + const cache = new AsyncStorageCache<TestData>(); + + const data = { a: 1, b: '2', d: { e: true } }; + await cache.set('key', data); + + expect(await asyncStorage.getItem('key')).toBe(JSON.stringify(data)); + expect(await cache.get('key')).toEqual(data); + }); + + it('should return undefined if get is called for a nonexistent key', async () => { + const cache = new AsyncStorageCache<string>(); + + expect(await cache.get('nonexistent')).toBeUndefined(); + }); + + it('should return the value if get is called for an existing key', async () => { + const cache = new AsyncStorageCache<string>(); + await cache.set('key', 'value'); + + expect(await cache.get('key')).toBe('value'); + }); + + it('should return the value after json parsing if get is called for an existing key', async () => { + const cache = new AsyncStorageCache<TestData>(); + const data = { a: 1, b: '2', d: { e: true } }; + await cache.set('key', data); + + expect(await cache.get('key')).toEqual(data); + }); + + it('should remove the key from async storage when remove is called', async () => { + const cache = new AsyncStorageCache<string>(); + await cache.set('key', 'value'); + await cache.remove('key'); + + expect(await asyncStorage.getItem('key')).toBeNull(); + }); + + it('should remove all keys from async storage when clear is called', async () => { + const cache = new AsyncStorageCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + + expect((await asyncStorage.getAllKeys()).length).toBe(2); + cache.clear(); + expect((await asyncStorage.getAllKeys()).length).toBe(0); + }); + + it('should return all keys when getKeys is called', async () => { + const cache = new AsyncStorageCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + + expect(await cache.getKeys()).toEqual(['key1', 'key2']); + }); + + it('should return an array of values for an array of keys when getBatched is called', async () => { + const cache = new AsyncStorageCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + + expect(await cache.getBatched(['key1', 'key2'])).toEqual(['value1', 'value2']); + }); +}); diff --git a/lib/utils/cache/async_storage_cache.react_native.ts b/lib/utils/cache/async_storage_cache.react_native.ts new file mode 100644 index 000000000..e5e76024e --- /dev/null +++ b/lib/utils/cache/async_storage_cache.react_native.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Maybe } from "../type"; +import { AsyncStore } from "./store"; +import { getDefaultAsyncStorage } from "../import.react_native/@react-native-async-storage/async-storage"; + +export class AsyncStorageCache<V> implements AsyncStore<V> { + public readonly operation = 'async'; + private asyncStorage = getDefaultAsyncStorage(); + + async get(key: string): Promise<V | undefined> { + const value = await this.asyncStorage.getItem(key); + return value ? JSON.parse(value) : undefined; + } + + async remove(key: string): Promise<unknown> { + return this.asyncStorage.removeItem(key); + } + + async set(key: string, val: V): Promise<unknown> { + return this.asyncStorage.setItem(key, JSON.stringify(val)); + } + + async clear(): Promise<unknown> { + return this.asyncStorage.clear(); + } + + async getKeys(): Promise<string[]> { + return [... await this.asyncStorage.getAllKeys()]; + } + + async getBatched(keys: string[]): Promise<Maybe<V>[]> { + const items = await this.asyncStorage.multiGet(keys); + return items.map(([key, value]) => value ? JSON.parse(value) : undefined); + } +} diff --git a/lib/utils/cache/cache.ts b/lib/utils/cache/cache.ts new file mode 100644 index 000000000..ada8a5ac6 --- /dev/null +++ b/lib/utils/cache/cache.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { OpType, OpValue } from '../../utils/type'; + +export interface OpCache<OP extends OpType, V> { + operation: OP; + save(key: string, value: V): OpValue<OP, unknown>; + lookup(key: string): OpValue<OP, V | undefined>; + reset(): OpValue<OP, unknown>; +} + +export type SyncCache<V> = OpCache<'sync', V>; +export type AsyncCache<V> = OpCache<'async', V>; + +export type Cache<V> = SyncCache<V> | AsyncCache<V>; + +export interface OpCacheWithRemove<OP extends OpType, V> extends OpCache<OP, V> { + remove(key: string): OpValue<OP, unknown>; +} + +export type SyncCacheWithRemove<V> = OpCacheWithRemove<'sync', V>; +export type AsyncCacheWithRemove<V> = OpCacheWithRemove<'async', V>; +export type CacheWithRemove<V> = SyncCacheWithRemove<V> | AsyncCacheWithRemove<V>; diff --git a/lib/utils/cache/in_memory_lru_cache.spec.ts b/lib/utils/cache/in_memory_lru_cache.spec.ts new file mode 100644 index 000000000..81c1e4a96 --- /dev/null +++ b/lib/utils/cache/in_memory_lru_cache.spec.ts @@ -0,0 +1,106 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, describe, it } from 'vitest'; +import { InMemoryLruCache } from './in_memory_lru_cache'; +import { wait } from '../../tests/testUtils'; + +describe('InMemoryLruCache', () => { + it('should save and get values correctly', () => { + const cache = new InMemoryLruCache<number>(2); + cache.save('a', 1); + cache.save('b', 2); + expect(cache.lookup('a')).toBe(1); + expect(cache.lookup('b')).toBe(2); + }); + + it('should return undefined for non-existent keys', () => { + const cache = new InMemoryLruCache<number>(2); + expect(cache.lookup('a')).toBe(undefined); + }); + + it('should return all keys in cache when getKeys is called', () => { + const cache = new InMemoryLruCache<number>(20); + cache.save('a', 1); + cache.save('b', 2); + cache.save('c', 3); + cache.save('d', 4); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['d', 'c', 'b', 'a'])); + }); + it('should evict least recently used keys when full', () => { + const cache = new InMemoryLruCache<number>(3); + cache.save('a', 1); + cache.save('b', 2); + cache.save('c', 3); + + expect(cache.lookup('b')).toBe(2); + expect(cache.lookup('c')).toBe(3); + expect(cache.lookup('a')).toBe(1); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['a', 'c', 'b'])); + + // key use order is now a c b. next insert should evict b + cache.save('d', 4); + expect(cache.lookup('b')).toBe(undefined); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['d', 'a', 'c'])); + + // key use order is now d a c. setting c should put it at the front + cache.save('c', 5); + + // key use order is now c d a. next insert should evict a + cache.save('e', 6); + expect(cache.lookup('a')).toBe(undefined); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['e', 'c', 'd'])); + + // key use order is now e c d. reading d should put it at the front + expect(cache.lookup('d')).toBe(4); + + // key use order is now d e c. next insert should evict c + cache.save('f', 7); + expect(cache.lookup('c')).toBe(undefined); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['f', 'd', 'e'])); + }); + + it('should not return expired values when get is called', async () => { + const cache = new InMemoryLruCache<number>(2, 100); + cache.save('a', 1); + cache.save('b', 2); + expect(cache.lookup('a')).toBe(1); + expect(cache.lookup('b')).toBe(2); + + await wait(150); + expect(cache.lookup('a')).toBe(undefined); + expect(cache.lookup('b')).toBe(undefined); + }); + + it('should remove values correctly', () => { + const cache = new InMemoryLruCache<number>(2); + cache.save('a', 1); + cache.save('b', 2); + cache.save('c', 3); + cache.remove('a'); + expect(cache.lookup('a')).toBe(undefined); + expect(cache.lookup('b')).toBe(2); + expect(cache.lookup('c')).toBe(3); + }); + + it('should clear all values correctly', () => { + const cache = new InMemoryLruCache<number>(2); + cache.save('a', 1); + cache.save('b', 2); + cache.reset(); + expect(cache.lookup('a')).toBe(undefined); + expect(cache.lookup('b')).toBe(undefined); + }); +}); diff --git a/lib/utils/cache/in_memory_lru_cache.ts b/lib/utils/cache/in_memory_lru_cache.ts new file mode 100644 index 000000000..6ed92d1fd --- /dev/null +++ b/lib/utils/cache/in_memory_lru_cache.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Maybe } from "../type"; +import { SyncCacheWithRemove } from "./cache"; + +type CacheElement<V> = { + value: V; + expiresAt?: number; +}; + +export class InMemoryLruCache<V> implements SyncCacheWithRemove<V> { + public operation = 'sync' as const; + private data: Map<string, CacheElement<V>> = new Map(); + private maxSize: number; + private ttl?: number; + + constructor(maxSize: number, ttl?: number) { + this.maxSize = maxSize; + this.ttl = ttl; + } + + lookup(key: string): Maybe<V> { + const element = this.data.get(key); + if (!element) return undefined; + this.data.delete(key); + + if (element.expiresAt && element.expiresAt <= Date.now()) { + return undefined; + } + + this.data.set(key, element); + return element.value; + } + + save(key: string, value: V): void { + this.data.delete(key); + + if (this.data.size === this.maxSize) { + const firstMapEntryKey = this.data.keys().next().value; + this.data.delete(firstMapEntryKey!); + } + + this.data.set(key, { + value, + expiresAt: this.ttl ? Date.now() + this.ttl : undefined, + }); + } + + remove(key: string): void { + this.data.delete(key); + } + + reset(): void { + this.data.clear(); + } + + getKeys(): string[] { + return Array.from(this.data.keys()); + } +} diff --git a/lib/utils/cache/local_storage_cache.browser.spec.ts b/lib/utils/cache/local_storage_cache.browser.spec.ts new file mode 100644 index 000000000..37e0735ba --- /dev/null +++ b/lib/utils/cache/local_storage_cache.browser.spec.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { LocalStorageCache } from './local_storage_cache.browser'; + +type TestData = { + a: number; + b: string; + d: { e: boolean }; +} + +describe('LocalStorageCache', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('should store a stringified value in local storage', () => { + const cache = new LocalStorageCache<TestData>(); + const data = { a: 1, b: '2', d: { e: true } }; + cache.set('key', data); + expect(localStorage.getItem('key')).toBe(JSON.stringify(data)); + }); + + it('should return undefined if get is called for a nonexistent key', () => { + const cache = new LocalStorageCache<string>(); + expect(cache.get('nonexistent')).toBeUndefined(); + }); + + it('should return the value if get is called for an existing key', () => { + const cache = new LocalStorageCache<string>(); + cache.set('key', 'value'); + expect(cache.get('key')).toBe('value'); + }); + + it('should return the value after json parsing if get is called for an existing key', () => { + const cache = new LocalStorageCache<TestData>(); + const data = { a: 1, b: '2', d: { e: true } }; + cache.set('key', data); + expect(cache.get('key')).toEqual(data); + }); + + it('should remove the key from local storage when remove is called', () => { + const cache = new LocalStorageCache<string>(); + cache.set('key', 'value'); + cache.remove('key'); + expect(localStorage.getItem('key')).toBeNull(); + }); + + it('should remove all keys from local storage when clear is called', () => { + const cache = new LocalStorageCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + expect(localStorage.length).toBe(2); + cache.clear(); + expect(localStorage.length).toBe(0); + }); + + it('should return all keys when getKeys is called', () => { + const cache = new LocalStorageCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + expect(cache.getKeys()).toEqual(['key1', 'key2']); + }); + + it('should return an array of values for an array of keys when getBatched is called', () => { + const cache = new LocalStorageCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + expect(cache.getBatched(['key1', 'key2'])).toEqual(['value1', 'value2']); + }); +}); diff --git a/lib/utils/cache/local_storage_cache.browser.ts b/lib/utils/cache/local_storage_cache.browser.ts new file mode 100644 index 000000000..b16d77571 --- /dev/null +++ b/lib/utils/cache/local_storage_cache.browser.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Maybe } from "../type"; +import { SyncStore } from "./store"; + +export class LocalStorageCache<V> implements SyncStore<V> { + public readonly operation = 'sync'; + + public set(key: string, value: V): void { + localStorage.setItem(key, JSON.stringify(value)); + } + + public get(key: string): Maybe<V> { + const value = localStorage.getItem(key); + return value ? JSON.parse(value) : undefined; + } + + public remove(key: string): void { + localStorage.removeItem(key); + } + + public clear(): void { + localStorage.clear(); + } + + public getKeys(): string[] { + const keys: string[] = []; + for(let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) { + keys.push(key); + } + } + return keys; + } + + getBatched(keys: string[]): Maybe<V>[] { + return keys.map((k) => this.get(k)); + } +} diff --git a/lib/utils/cache/store.spec.ts b/lib/utils/cache/store.spec.ts new file mode 100644 index 000000000..a99226844 --- /dev/null +++ b/lib/utils/cache/store.spec.ts @@ -0,0 +1,291 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import { SyncPrefixStore, AsyncPrefixStore } from './store'; +import { getMockSyncCache, getMockAsyncCache } from '../../tests/mock/mock_cache'; + +describe('SyncPrefixStore', () => { + describe('set', () => { + it('should add prefix to key when setting in the underlying cache', () => { + const cache = getMockSyncCache<string>(); + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + prefixCache.set('key', 'value'); + expect(cache.get('prefix:key')).toEqual('value'); + }); + + it('should transform value when setting in the underlying cache', () => { + const cache = getMockSyncCache<string>(); + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + prefixCache.set('key', 'value'); + expect(cache.get('prefix:key')).toEqual('VALUE'); + }); + + it('should work correctly with empty prefix', () => { + const cache = getMockSyncCache<string>(); + const prefixCache = new SyncPrefixStore(cache, '', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + prefixCache.set('key', 'value'); + expect(cache.get('key')).toEqual('VALUE'); + }); + }); + + describe('get', () => { + it('should remove prefix from key when getting from the underlying cache', () => { + const cache = getMockSyncCache<string>(); + cache.set('prefix:key', 'value'); + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + expect(prefixCache.get('key')).toEqual('value'); + }); + + it('should transform value after getting from the underlying cache', () => { + const cache = getMockSyncCache<string>(); + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + cache.set('prefix:key', 'VALUE'); + expect(prefixCache.get('key')).toEqual('value'); + }); + + + it('should work correctly with empty prefix', () => { + const cache = getMockSyncCache<string>(); + const prefixCache = new SyncPrefixStore(cache, '', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + cache.set('key', 'VALUE'); + expect(prefixCache.get('key')).toEqual('value'); + }); + }); + + describe('remove', () => { + it('should remove the correct value from the underlying cache', () => { + const cache = getMockSyncCache<string>(); + cache.set('prefix:key', 'value'); + cache.set('key', 'value'); + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + prefixCache.remove('key'); + expect(cache.get('prefix:key')).toBeUndefined(); + expect(cache.get('key')).toEqual('value'); + }); + + it('should work with empty prefix', () => { + const cache = getMockSyncCache<string>(); + cache.set('key', 'value'); + const prefixCache = new SyncPrefixStore(cache, '', (v) => v, (v) => v); + prefixCache.remove('key'); + expect(cache.get('key')).toBeUndefined(); + }); + }); + + describe('getKeys', () => { + it('should return keys with correct prefix', () => { + const cache = getMockSyncCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('prefix:key3', 'value1'); + cache.set('prefix:key4', 'value2'); + + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + + const keys = prefixCache.getKeys(); + expect(keys).toEqual(expect.arrayContaining(['key3', 'key4'])); + }); + + it('should work with empty prefix', () => { + const cache = getMockSyncCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + + const prefixCache = new SyncPrefixStore(cache, '', (v) => v, (v) => v); + + const keys = prefixCache.getKeys(); + expect(keys).toEqual(expect.arrayContaining(['key1', 'key2'])); + }); + }); + + describe('getBatched', () => { + it('should return values with correct prefix', () => { + const cache = getMockSyncCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('key3', 'value3'); + cache.set('prefix:key1', 'prefix:value1'); + cache.set('prefix:key2', 'prefix:value2'); + + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + + const values = prefixCache.getBatched(['key1', 'key2', 'key3']); + expect(values).toEqual(expect.arrayContaining(['prefix:value1', 'prefix:value2', undefined])); + }); + + it('should transform values after getting from the underlying cache', () => { + const cache = getMockSyncCache<string>(); + cache.set('key1', 'VALUE1'); + cache.set('key2', 'VALUE2'); + cache.set('key3', 'VALUE3'); + cache.set('prefix:key1', 'PREFIX:VALUE1'); + cache.set('prefix:key2', 'PREFIX:VALUE2'); + + const prefixCache = new SyncPrefixStore(cache, 'prefix:', (v) => v.toLocaleLowerCase(), (v) => v.toUpperCase()); + + const values = prefixCache.getBatched(['key1', 'key2', 'key3']); + expect(values).toEqual(expect.arrayContaining(['prefix:value1', 'prefix:value2', undefined])); + }); + + it('should work with empty prefix', () => { + const cache = getMockSyncCache<string>(); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + + const prefixCache = new SyncPrefixStore(cache, '', (v) => v, (v) => v); + + const values = prefixCache.getBatched(['key1', 'key2']); + expect(values).toEqual(expect.arrayContaining(['value1', 'value2'])); + }); + }); +}); + +describe('AsyncPrefixStore', () => { + describe('set', () => { + it('should add prefix to key when setting in the underlying cache', async () => { + const cache = getMockAsyncCache<string>(); + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + await prefixCache.set('key', 'value'); + expect(await cache.get('prefix:key')).toEqual('value'); + }); + + it('should transform value when setting in the underlying cache', async () => { + const cache = getMockAsyncCache<string>(); + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + await prefixCache.set('key', 'value'); + expect(await cache.get('prefix:key')).toEqual('VALUE'); + }); + + it('should work correctly with empty prefix', async () => { + const cache = getMockAsyncCache<string>(); + const prefixCache = new AsyncPrefixStore(cache, '', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + await prefixCache.set('key', 'value'); + expect(await cache.get('key')).toEqual('VALUE'); + }); + }); + + describe('get', () => { + it('should remove prefix from key when getting from the underlying cache', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('prefix:key', 'value'); + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + expect(await prefixCache.get('key')).toEqual('value'); + }); + + it('should transform value after getting from the underlying cache', async () => { + const cache = getMockAsyncCache<string>(); + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + await cache.set('prefix:key', 'VALUE'); + expect(await prefixCache.get('key')).toEqual('value'); + }); + + + it('should work correctly with empty prefix', async () => { + const cache = getMockAsyncCache<string>(); + const prefixCache = new AsyncPrefixStore(cache, '', (v) => v.toLowerCase(), (v) => v.toUpperCase()); + await cache.set('key', 'VALUE'); + expect(await prefixCache.get('key')).toEqual('value'); + }); + }); + + describe('remove', () => { + it('should remove the correct value from the underlying cache', async () => { + const cache = getMockAsyncCache<string>(); + cache.set('prefix:key', 'value'); + cache.set('key', 'value'); + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + await prefixCache.remove('key'); + expect(await cache.get('prefix:key')).toBeUndefined(); + expect(await cache.get('key')).toEqual('value'); + }); + + it('should work with empty prefix', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('key', 'value'); + const prefixCache = new AsyncPrefixStore(cache, '', (v) => v, (v) => v); + await prefixCache.remove('key'); + expect(await cache.get('key')).toBeUndefined(); + }); + }); + + describe('getKeys', () => { + it('should return keys with correct prefix', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + await cache.set('prefix:key3', 'value1'); + await cache.set('prefix:key4', 'value2'); + + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + + const keys = await prefixCache.getKeys(); + expect(keys).toEqual(expect.arrayContaining(['key3', 'key4'])); + }); + + it('should work with empty prefix', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + + const prefixCache = new AsyncPrefixStore(cache, '', (v) => v, (v) => v); + + const keys = await prefixCache.getKeys(); + expect(keys).toEqual(expect.arrayContaining(['key1', 'key2'])); + }); + }); + + describe('getBatched', () => { + it('should return values with correct prefix', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + await cache.set('key3', 'value3'); + await cache.set('prefix:key1', 'prefix:value1'); + await cache.set('prefix:key2', 'prefix:value2'); + + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v, (v) => v); + + const values = await prefixCache.getBatched(['key1', 'key2', 'key3']); + expect(values).toEqual(expect.arrayContaining(['prefix:value1', 'prefix:value2', undefined])); + }); + + it('should transform values after getting from the underlying cache', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('key1', 'VALUE1'); + await cache.set('key2', 'VALUE2'); + await cache.set('key3', 'VALUE3'); + await cache.set('prefix:key1', 'PREFIX:VALUE1'); + await cache.set('prefix:key2', 'PREFIX:VALUE2'); + + const prefixCache = new AsyncPrefixStore(cache, 'prefix:', (v) => v.toLocaleLowerCase(), (v) => v.toUpperCase()); + + const values = await prefixCache.getBatched(['key1', 'key2', 'key3']); + expect(values).toEqual(expect.arrayContaining(['prefix:value1', 'prefix:value2', undefined])); + }); + + it('should work with empty prefix', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set('key1', 'value1'); + await cache.set('key2', 'value2'); + + const prefixCache = new AsyncPrefixStore(cache, '', (v) => v, (v) => v); + + const values = await prefixCache.getBatched(['key1', 'key2']); + expect(values).toEqual(expect.arrayContaining(['value1', 'value2'])); + }); + }); +}); \ No newline at end of file diff --git a/lib/utils/cache/store.ts b/lib/utils/cache/store.ts new file mode 100644 index 000000000..c13852f65 --- /dev/null +++ b/lib/utils/cache/store.ts @@ -0,0 +1,174 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Transformer } from '../../utils/type'; +import { Maybe } from '../../utils/type'; +import { OpType, OpValue } from '../../utils/type'; + +export interface OpStore<OP extends OpType, V> { + operation: OP; + set(key: string, value: V): OpValue<OP, unknown>; + get(key: string): OpValue<OP, Maybe<V>>; + remove(key: string): OpValue<OP, unknown>; + getKeys(): OpValue<OP, string[]>; +} + +export type SyncStore<V> = OpStore<'sync', V>; +export type AsyncStore<V> = OpStore<'async', V>; +export type Store<V> = SyncStore<V> | AsyncStore<V>; + +export abstract class SyncStoreWithBatchedGet<V> implements SyncStore<V> { + operation = 'sync' as const; + abstract set(key: string, value: V): unknown; + abstract get(key: string): Maybe<V>; + abstract remove(key: string): unknown; + abstract getKeys(): string[]; + abstract getBatched(keys: string[]): Maybe<V>[]; +} + +export abstract class AsyncStoreWithBatchedGet<V> implements AsyncStore<V> { + operation = 'async' as const; + abstract set(key: string, value: V): Promise<unknown>; + abstract get(key: string): Promise<Maybe<V>>; + abstract remove(key: string): Promise<unknown>; + abstract getKeys(): Promise<string[]>; + abstract getBatched(keys: string[]): Promise<Maybe<V>[]>; +} + +export const getBatchedSync = <V>(store: SyncStore<V>, keys: string[]): Maybe<V>[] => { + if (store instanceof SyncStoreWithBatchedGet) { + return store.getBatched(keys); + } + return keys.map((key) => store.get(key)); +}; + +export const getBatchedAsync = <V>(store: AsyncStore<V>, keys: string[]): Promise<Maybe<V>[]> => { + if (store instanceof AsyncStoreWithBatchedGet) { + return store.getBatched(keys); + } + return Promise.all(keys.map((key) => store.get(key))); +}; + +export class SyncPrefixStore<U, V> extends SyncStoreWithBatchedGet<V> implements SyncStore<V> { + private store: SyncStore<U>; + private prefix: string; + private transformGet: Transformer<U, V>; + private transformSet: Transformer<V, U>; + + public readonly operation = 'sync'; + + constructor( + store: SyncStore<U>, + prefix: string, + transformGet: Transformer<U, V>, + transformSet: Transformer<V, U> + ) { + super(); + this.store = store; + this.prefix = prefix; + this.transformGet = transformGet; + this.transformSet = transformSet; + } + + private addPrefix(key: string): string { + return `${this.prefix}${key}`; + } + + private removePrefix(key: string): string { + return key.substring(this.prefix.length); + } + + set(key: string, value: V): unknown { + return this.store.set(this.addPrefix(key), this.transformSet(value)); + } + + get(key: string): V | undefined { + const value = this.store.get(this.addPrefix(key)); + return value ? this.transformGet(value) : undefined; + } + + remove(key: string): unknown { + return this.store.remove(this.addPrefix(key)); + } + + private getInternalKeys(): string[] { + return this.store.getKeys().filter((key) => key.startsWith(this.prefix)); + } + + getKeys(): string[] { + return this.getInternalKeys().map((key) => this.removePrefix(key)); + } + + getBatched(keys: string[]): Maybe<V>[] { + return getBatchedSync(this.store, keys.map((key) => this.addPrefix(key))) + .map((value) => value ? this.transformGet(value) : undefined); + } +} + +export class AsyncPrefixStore<U, V> implements AsyncStore<V> { + private cache: AsyncStore<U>; + private prefix: string; + private transformGet: Transformer<U, V>; + private transformSet: Transformer<V, U>; + + public readonly operation = 'async'; + + constructor( + cache: AsyncStore<U>, + prefix: string, + transformGet: Transformer<U, V>, + transformSet: Transformer<V, U> + ) { + this.cache = cache; + this.prefix = prefix; + this.transformGet = transformGet; + this.transformSet = transformSet; + } + + private addPrefix(key: string): string { + return `${this.prefix}${key}`; + } + + private removePrefix(key: string): string { + return key.substring(this.prefix.length); + } + + set(key: string, value: V): Promise<unknown> { + return this.cache.set(this.addPrefix(key), this.transformSet(value)); + } + + async get(key: string): Promise<V | undefined> { + const value = await this.cache.get(this.addPrefix(key)); + return value ? this.transformGet(value) : undefined; + } + + remove(key: string): Promise<unknown> { + return this.cache.remove(this.addPrefix(key)); + } + + private async getInternalKeys(): Promise<string[]> { + return this.cache.getKeys().then((keys) => keys.filter((key) => key.startsWith(this.prefix))); + } + + async getKeys(): Promise<string[]> { + return this.getInternalKeys().then((keys) => keys.map((key) => this.removePrefix(key))); + } + + async getBatched(keys: string[]): Promise<Maybe<V>[]> { + const values = await getBatchedAsync(this.cache, keys.map((key) => this.addPrefix(key))); + return values.map((value) => value ? this.transformGet(value) : undefined); + } +} diff --git a/lib/utils/cache/store_validator.ts b/lib/utils/cache/store_validator.ts new file mode 100644 index 000000000..949bb25c3 --- /dev/null +++ b/lib/utils/cache/store_validator.ts @@ -0,0 +1,36 @@ + +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const INVALID_STORE = 'Invalid store'; +export const INVALID_STORE_METHOD = 'Invalid store method %s'; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const validateStore = (store: any): void => { + const errors = []; + if (!store || typeof store !== 'object') { + throw new Error(INVALID_STORE); + } + + for (const method of ['set', 'get', 'remove', 'getKeys']) { + if (typeof store[method] !== 'function') { + errors.push(INVALID_STORE_METHOD.replace('%s', method)); + } + } + + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } +} diff --git a/lib/utils/config_validator/index.spec.ts b/lib/utils/config_validator/index.spec.ts new file mode 100644 index 000000000..c8496ecc4 --- /dev/null +++ b/lib/utils/config_validator/index.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import configValidator from './'; +import testData from '../../tests/test_data'; +import { INVALID_DATAFILE_MALFORMED, INVALID_DATAFILE_VERSION, NO_DATAFILE_SPECIFIED } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +describe('validate', () => { + it('should complain if datafile is not provided', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => configValidator.validateDatafile()).toThrow(OptimizelyError); + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + configValidator.validateDatafile(); + } catch (err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(NO_DATAFILE_SPECIFIED); + } + }); + + it('should complain if datafile is malformed', () => { + expect(() => configValidator.validateDatafile('abc')).toThrow( OptimizelyError); + + try { + configValidator.validateDatafile('abc'); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_DATAFILE_MALFORMED); + } + }); + + it('should complain if datafile version is not supported', () => { + expect(() => configValidator.validateDatafile(JSON.stringify(testData.getUnsupportedVersionConfig())).toThrow(OptimizelyError)); + + try { + configValidator.validateDatafile(JSON.stringify(testData.getUnsupportedVersionConfig())); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_DATAFILE_VERSION); + expect(err.params).toEqual(['5']); + } + }); + + it('should not complain if datafile is valid', () => { + expect(() => configValidator.validateDatafile(JSON.stringify(testData.getTestProjectConfig())).not.toThrowError()); + }); +}); diff --git a/packages/optimizely-sdk/lib/utils/config_validator/index.tests.js b/lib/utils/config_validator/index.tests.js similarity index 53% rename from packages/optimizely-sdk/lib/utils/config_validator/index.tests.js rename to lib/utils/config_validator/index.tests.js index abd697ad1..2680a07da 100644 --- a/packages/optimizely-sdk/lib/utils/config_validator/index.tests.js +++ b/lib/utils/config_validator/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2016, 2018, Optimizely + * Copyright 2016, 2018-2020, 2022, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,57 +13,70 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var chai = require('chai'); -var assert = chai.assert; -var configValidator = require('./'); -var sprintf = require('sprintf-js').sprintf; -var testData = require('../../tests/test_data') +import { assert } from 'chai'; +import { sprintf } from '../../utils/fns'; -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; +import configValidator from './'; +import testData from '../../tests/test_data'; +import { + INVALID_DATAFILE_MALFORMED, + INVALID_DATAFILE_VERSION, + INVALID_ERROR_HANDLER, + INVALID_EVENT_DISPATCHER, + INVALID_LOGGER, + NO_DATAFILE_SPECIFIED, +} from 'error_message'; describe('lib/utils/config_validator', function() { describe('APIs', function() { describe('validate', function() { - it('should complain if the provided error handler is invalid', function() { - assert.throws(function() { + it.skip('should complain if the provided error handler is invalid', function() { + const ex = assert.throws(function() { configValidator.validate({ errorHandler: {}, }); - }, sprintf(ERROR_MESSAGES.INVALID_ERROR_HANDLER, 'CONFIG_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_ERROR_HANDLER); }); - it('should complain if the provided event dispatcher is invalid', function() { - assert.throws(function() { + it.skip('should complain if the provided event dispatcher is invalid', function() { + const ex = assert.throws(function() { configValidator.validate({ eventDispatcher: {}, }); - }, sprintf(ERROR_MESSAGES.INVALID_EVENT_DISPATCHER, 'CONFIG_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_EVENT_DISPATCHER); }); - it('should complain if the provided logger is invalid', function() { - assert.throws(function() { + it.skip('should complain if the provided logger is invalid', function() { + const ex = assert.throws(function() { configValidator.validate({ logger: {}, }); - }, sprintf(ERROR_MESSAGES.INVALID_LOGGER, 'CONFIG_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_LOGGER); }); it('should complain if datafile is not provided', function() { - assert.throws(function() { + const ex = assert.throws(function() { configValidator.validateDatafile(); - }, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'CONFIG_VALIDATOR')); + }); + assert.equal(ex.baseMessage, NO_DATAFILE_SPECIFIED); }); it('should complain if datafile is malformed', function() { - assert.throws(function() { + const ex = assert.throws(function() { configValidator.validateDatafile('abc'); - }, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_MALFORMED, 'CONFIG_VALIDATOR')); + }); + assert.equal(ex.baseMessage, INVALID_DATAFILE_MALFORMED); }); it('should complain if datafile version is not supported', function() { - assert.throws(function() { + const ex = assert.throws(function() { configValidator.validateDatafile(JSON.stringify(testData.getUnsupportedVersionConfig())); - }, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_VERSION, 'CONFIG_VALIDATOR', '5')); + }); + assert.equal(ex.baseMessage, INVALID_DATAFILE_VERSION); + assert.deepEqual(ex.params, ['5']); }); it('should not complain if datafile is valid', function() { diff --git a/lib/utils/config_validator/index.ts b/lib/utils/config_validator/index.ts new file mode 100644 index 000000000..49c927f49 --- /dev/null +++ b/lib/utils/config_validator/index.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2016, 2018-2020, 2022, 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + DATAFILE_VERSIONS, +} from '../enums'; +import { + INVALID_DATAFILE_MALFORMED, + INVALID_DATAFILE_VERSION, + NO_DATAFILE_SPECIFIED, +} from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +const SUPPORTED_VERSIONS = [DATAFILE_VERSIONS.V2, DATAFILE_VERSIONS.V3, DATAFILE_VERSIONS.V4]; + +/** + * Validates the datafile + * @param {Object|string} datafile + * @return {Object} The datafile object if the datafile is valid + * @throws If the datafile is not valid for any of the following reasons: + - The datafile string is undefined + - The datafile string cannot be parsed as a JSON object + - The datafile version is not supported + */ +// eslint-disable-next-line +export const validateDatafile = function(datafile: unknown): any { + if (!datafile) { + throw new OptimizelyError(NO_DATAFILE_SPECIFIED); + } + if (typeof datafile === 'string') { + // Attempt to parse the datafile string + try { + datafile = JSON.parse(datafile); + } catch (ex) { + throw new OptimizelyError(INVALID_DATAFILE_MALFORMED); + } + } + if (typeof datafile === 'object' && !Array.isArray(datafile) && datafile !== null) { + if (SUPPORTED_VERSIONS.indexOf(datafile['version' as keyof unknown]) === -1) { + throw new OptimizelyError(INVALID_DATAFILE_VERSION, datafile['version' as keyof unknown]); + } + } else { + throw new OptimizelyError(INVALID_DATAFILE_MALFORMED); + } + + return datafile; +}; + +export default { + validateDatafile: validateDatafile, +} diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts new file mode 100644 index 000000000..99c254e5e --- /dev/null +++ b/lib/utils/enums/index.ts @@ -0,0 +1,106 @@ +/** + * Copyright 2016-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains global enums used throughout the library + */ +export const LOG_LEVEL = { + NOTSET: 0, + DEBUG: 1, + INFO: 2, + WARNING: 3, + ERROR: 4, +}; + + +export const enum RESERVED_EVENT_KEYWORDS { + REVENUE = 'revenue', + VALUE = 'value', +} + +export const CONTROL_ATTRIBUTES = { + BOT_FILTERING: '$opt_bot_filtering', + BUCKETING_ID: '$opt_bucketing_id', + STICKY_BUCKETING_KEY: '$opt_experiment_bucket_map', + USER_AGENT: '$opt_user_agent', +}; + +export const JAVASCRIPT_CLIENT_ENGINE = 'javascript-sdk'; +export const NODE_CLIENT_ENGINE = 'node-sdk'; +export const REACT_NATIVE_JS_CLIENT_ENGINE = 'react-native-js-sdk'; +export const CLIENT_VERSION = '6.1.0'; + +/* + * Represents the source of a decision for feature management. When a feature + * is accessed through isFeatureEnabled or getVariableValue APIs, the decision + * source is used to decide whether to dispatch an impression event to + * Optimizely. + */ +export const DECISION_SOURCES = { + FEATURE_TEST: 'feature-test', + ROLLOUT: 'rollout', + EXPERIMENT: 'experiment', + HOLDOUT: 'holdout', +} as const; + +export type DecisionSource = typeof DECISION_SOURCES[keyof typeof DECISION_SOURCES]; + +export const AUDIENCE_EVALUATION_TYPES = { + RULE: 'rule', + EXPERIMENT: 'experiment', +}; + +/* + * Possible types of variables attached to features + */ +export const FEATURE_VARIABLE_TYPES = { + BOOLEAN: 'boolean', + DOUBLE: 'double', + INTEGER: 'integer', + STRING: 'string', + JSON: 'json', +}; + +/* + * Supported datafile versions + */ +export const DATAFILE_VERSIONS = { + V2: '2', + V3: '3', + V4: '4', +}; + +/* + * Pre-Release and Build symbols + */ +export const enum VERSION_TYPE { + PRE_RELEASE_VERSION_DELIMITER = '-', + BUILD_VERSION_DELIMITER = '+', +} + +export const DECISION_MESSAGES = { + SDK_NOT_READY: 'Optimizely SDK not configured properly yet.', + FLAG_KEY_INVALID: 'No flag was found for key "%s".', + VARIABLE_VALUE_INVALID: 'Variable value for key "%s" is invalid or wrong type.', +}; + +/** + * Default milliseconds before request timeout + */ +export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute + +export const DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 * 1000; // 30 minutes +export const DEFAULT_CMAB_CACHE_SIZE = 1000; diff --git a/lib/utils/event_emitter/event_emitter.spec.ts b/lib/utils/event_emitter/event_emitter.spec.ts new file mode 100644 index 000000000..fb5cfe441 --- /dev/null +++ b/lib/utils/event_emitter/event_emitter.spec.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { it, vi, expect } from 'vitest'; + +import { EventEmitter } from './event_emitter'; + +it('should call all registered listeners correctly on emit event', () => { + const emitter = new EventEmitter<{ foo: number, bar: string, baz: boolean}>(); + const fooListener1 = vi.fn(); + const fooListener2 = vi.fn(); + + emitter.on('foo', fooListener1); + emitter.on('foo', fooListener2); + + const barListener1 = vi.fn(); + const barListener2 = vi.fn(); + + emitter.on('bar', barListener1); + emitter.on('bar', barListener2); + + const bazListener = vi.fn(); + emitter.on('baz', bazListener); + + emitter.emit('foo', 1); + emitter.emit('bar', 'hello'); + + expect(fooListener1).toHaveBeenCalledOnce(); + expect(fooListener1).toHaveBeenCalledWith(1); + expect(fooListener2).toHaveBeenCalledOnce(); + expect(fooListener2).toHaveBeenCalledWith(1); + expect(barListener1).toHaveBeenCalledOnce(); + expect(barListener1).toHaveBeenCalledWith('hello'); + expect(barListener2).toHaveBeenCalledOnce(); + expect(barListener2).toHaveBeenCalledWith('hello'); + + expect(bazListener).not.toHaveBeenCalled(); +}); + +it('should remove listeners correctly when the function returned from on is called', () => { + const emitter = new EventEmitter<{ foo: number, bar: string }>(); + const fooListener1 = vi.fn(); + const fooListener2 = vi.fn(); + + const dispose = emitter.on('foo', fooListener1); + emitter.on('foo', fooListener2); + + const barListener1 = vi.fn(); + const barListener2 = vi.fn(); + + emitter.on('bar', barListener1); + emitter.on('bar', barListener2); + + dispose(); + emitter.emit('foo', 1); + emitter.emit('bar', 'hello'); + + expect(fooListener1).not.toHaveBeenCalled(); + expect(fooListener2).toHaveBeenCalledOnce(); + expect(fooListener2).toHaveBeenCalledWith(1); + expect(barListener1).toHaveBeenCalledWith('hello'); + expect(barListener1).toHaveBeenCalledWith('hello'); +}) + +it('should remove all listeners when removeAllListeners() is called', () => { + const emitter = new EventEmitter<{ foo: number, bar: string, baz: boolean}>(); + const fooListener1 = vi.fn(); + const fooListener2 = vi.fn(); + + emitter.on('foo', fooListener1); + emitter.on('foo', fooListener2); + + const barListener1 = vi.fn(); + const barListener2 = vi.fn(); + + emitter.on('bar', barListener1); + emitter.on('bar', barListener2); + + emitter.removeAllListeners(); + + emitter.emit('foo', 1); + emitter.emit('bar', 'hello'); + + expect(fooListener1).not.toHaveBeenCalled(); + expect(fooListener2).not.toHaveBeenCalled(); + expect(barListener1).not.toHaveBeenCalled(); + expect(barListener2).not.toHaveBeenCalled(); +}); diff --git a/lib/utils/event_emitter/event_emitter.ts b/lib/utils/event_emitter/event_emitter.ts new file mode 100644 index 000000000..6bfa57f8d --- /dev/null +++ b/lib/utils/event_emitter/event_emitter.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Fn } from "../type"; + +type Consumer<T> = (arg: T) => void; + +type Listeners<T> = { + [Key in keyof T]?: Map<number, Consumer<T[Key]>>; +}; + +export class EventEmitter<T> { + private id = 0; + private listeners: Listeners<T> = {} as Listeners<T>; + + on<E extends keyof T>(eventName: E, listener: Consumer<T[E]>): Fn { + if (!this.listeners[eventName]) { + this.listeners[eventName] = new Map(); + } + + const curId = this.id++; + this.listeners[eventName]?.set(curId, listener); + return () => { + this.listeners[eventName]?.delete(curId); + } + } + + emit<E extends keyof T>(eventName: E, data: T[E]): void { + const listeners = this.listeners[eventName]; + if (listeners) { + listeners.forEach(listener => { + listener(data); + }); + } + } + + removeListeners<E extends keyof T>(eventName: E): void { + this.listeners[eventName]?.clear(); + } + + removeAllListeners(): void { + this.listeners = {}; + } +} diff --git a/lib/utils/event_tag_utils/index.spec.ts b/lib/utils/event_tag_utils/index.spec.ts new file mode 100644 index 000000000..a1208b601 --- /dev/null +++ b/lib/utils/event_tag_utils/index.spec.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import * as eventTagUtils from './'; +import { + FAILED_TO_PARSE_REVENUE, + PARSED_REVENUE_VALUE, + PARSED_NUMERIC_VALUE, + FAILED_TO_PARSE_VALUE, +} from 'log_message'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { LoggerFacade } from '../../logging/logger'; + +describe('getRevenueValue', () => { + let logger: LoggerFacade; + + beforeEach(() => { + logger = getMockLogger(); + }); + + it('should return the parseed integer for a valid revenue value', () => { + let parsedRevenueValue = eventTagUtils.getRevenueValue({ revenue: '1337' }, logger); + + expect(parsedRevenueValue).toBe(1337); + expect(logger.info).toHaveBeenCalledWith(PARSED_REVENUE_VALUE, 1337); + + parsedRevenueValue = eventTagUtils.getRevenueValue({ revenue: '13.37' }, logger); + + expect(parsedRevenueValue).toBe(13); + }); + + it('should return null and log a message for invalid value', () => { + const parsedRevenueValue = eventTagUtils.getRevenueValue({ revenue: 'invalid' }, logger); + + expect(parsedRevenueValue).toBe(null); + expect(logger.info).toHaveBeenCalledWith(FAILED_TO_PARSE_REVENUE, 'invalid'); + }); + + it('should return null if the revenue value is not present in the event tags', () => { + const parsedRevenueValue = eventTagUtils.getRevenueValue({ not_revenue: '1337' }, logger); + + expect(parsedRevenueValue).toBe(null); + }); +}); + +describe('getEventValue', () => { + let logger: LoggerFacade; + + beforeEach(() => { + logger = getMockLogger(); + }); + + it('should return the parsed integer for a valid numeric value', () => { + let parsedEventValue = eventTagUtils.getEventValue({ value: '1337' }, logger); + + expect(parsedEventValue).toBe(1337); + expect(logger.info).toHaveBeenCalledWith(PARSED_NUMERIC_VALUE, 1337); + + parsedEventValue = eventTagUtils.getEventValue({ value: '13.37' }, logger); + expect(parsedEventValue).toBe(13.37); + }); + + it('should return null and log a message for invalid value', () => { + const parsedNumericValue = eventTagUtils.getEventValue({ value: 'invalid' }, logger); + + expect(parsedNumericValue).toBe(null); + expect(logger.info).toHaveBeenCalledWith(FAILED_TO_PARSE_VALUE, 'invalid'); + }); + + it('should return null if the value is not present in the event tags', () => { + const parsedNumericValue = eventTagUtils.getEventValue({ not_value: '13.37' }, logger); + + expect(parsedNumericValue).toBe(null); + }); +}) diff --git a/packages/optimizely-sdk/lib/utils/event_tag_utils/index.tests.js b/lib/utils/event_tag_utils/index.tests.js similarity index 52% rename from packages/optimizely-sdk/lib/utils/event_tag_utils/index.tests.js rename to lib/utils/event_tag_utils/index.tests.js index 13add0db7..f1f81a1fb 100644 --- a/packages/optimizely-sdk/lib/utils/event_tag_utils/index.tests.js +++ b/lib/utils/event_tag_utils/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2017, Optimizely + * Copyright 2017, 2020, 2022, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,35 +13,53 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var chai = require('chai'); -var assert = chai.assert; -var sinon = require('sinon'); -var eventTagUtils = require('./'); +import sinon from 'sinon'; +import { assert } from 'chai'; +import { sprintf } from '../../utils/fns'; + +import * as eventTagUtils from './'; +import { FAILED_TO_PARSE_REVENUE, PARSED_REVENUE_VALUE, PARSED_NUMERIC_VALUE, FAILED_TO_PARSE_VALUE } from 'log_message'; + +var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); + +var createLogger = () => ({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + child: () => createLogger(), +}) describe('lib/utils/event_tag_utils', function() { var mockLogger; beforeEach(function() { - mockLogger = { - log: sinon.stub(), - }; + mockLogger = createLogger(); + sinon.stub(mockLogger, 'info'); }); describe('APIs', function() { describe('getRevenueValue', function() { describe('the revenue value is a valid number', function() { it('should return the parsed integer for the revenue value', function() { - var parsedRevenueValue = eventTagUtils.getRevenueValue({ - revenue: '1337', - }, mockLogger); + var parsedRevenueValue = eventTagUtils.getRevenueValue( + { + revenue: '1337', + }, + mockLogger + ); assert.strictEqual(parsedRevenueValue, 1337); - var logMessage = mockLogger.log.args[0][1]; - assert.strictEqual(logMessage, 'EVENT_TAG_UTILS: Parsed revenue value "1337" from event tags.'); + + assert.strictEqual(mockLogger.info.args[0][0], PARSED_REVENUE_VALUE); + assert.strictEqual(mockLogger.info.args[0][1], 1337); // test out a float - parsedRevenueValue = eventTagUtils.getRevenueValue({ - revenue: '13.37', - }, mockLogger); + parsedRevenueValue = eventTagUtils.getRevenueValue( + { + revenue: '13.37', + }, + mockLogger + ); assert.strictEqual(parsedRevenueValue, 13); }); @@ -49,22 +67,28 @@ describe('lib/utils/event_tag_utils', function() { describe('the revenue value is not a valid number', function() { it('should return null and log a message', function() { - var parsedRevenueValue = eventTagUtils.getRevenueValue({ - revenue: 'invalid', - }, mockLogger); + var parsedRevenueValue = eventTagUtils.getRevenueValue( + { + revenue: 'invalid', + }, + mockLogger + ); assert.strictEqual(parsedRevenueValue, null); - var logMessage = mockLogger.log.args[0][1]; - assert.strictEqual(logMessage, 'EVENT_TAG_UTILS: Failed to parse revenue value "invalid" from event tags.'); + assert.strictEqual(mockLogger.info.args[0][0], FAILED_TO_PARSE_REVENUE); + assert.strictEqual(mockLogger.info.args[0][1], 'invalid'); }); }); describe('the revenue value is not present in the event tags', function() { it('should return null', function() { - var parsedRevenueValue = eventTagUtils.getRevenueValue({ - not_revenue: '1337', - }, mockLogger); + var parsedRevenueValue = eventTagUtils.getRevenueValue( + { + not_revenue: '1337', + }, + mockLogger + ); assert.strictEqual(parsedRevenueValue, null); }); @@ -74,18 +98,25 @@ describe('lib/utils/event_tag_utils', function() { describe('getNumericValue', function() { describe('the event value is a valid number', function() { it('should return the parsed integer for the event value', function() { - var parsedEventValue = eventTagUtils.getEventValue({ - value: '1337', - }, mockLogger); + var parsedEventValue = eventTagUtils.getEventValue( + { + value: '1337', + }, + mockLogger + ); assert.strictEqual(parsedEventValue, 1337); - var logMessage = mockLogger.log.args[0][1]; - assert.strictEqual(logMessage, 'EVENT_TAG_UTILS: Parsed event value "1337" from event tags.'); + + assert.strictEqual(mockLogger.info.args[0][0], PARSED_NUMERIC_VALUE); + assert.strictEqual(mockLogger.info.args[0][1], 1337); // test out a float - parsedEventValue = eventTagUtils.getEventValue({ - value: '13.37', - }, mockLogger); + parsedEventValue = eventTagUtils.getEventValue( + { + value: '13.37', + }, + mockLogger + ); assert.strictEqual(parsedEventValue, 13.37); }); @@ -93,22 +124,28 @@ describe('lib/utils/event_tag_utils', function() { describe('the event value is not a valid number', function() { it('should return null and log a message', function() { - var parsedEventValue = eventTagUtils.getEventValue({ - value: 'invalid', - }, mockLogger); + var parsedEventValue = eventTagUtils.getEventValue( + { + value: 'invalid', + }, + mockLogger + ); assert.strictEqual(parsedEventValue, null); - var logMessage = mockLogger.log.args[0][1]; - assert.strictEqual(logMessage, 'EVENT_TAG_UTILS: Failed to parse event value "invalid" from event tags.'); + assert.strictEqual(mockLogger.info.args[0][0], FAILED_TO_PARSE_VALUE); + assert.strictEqual(mockLogger.info.args[0][1], 'invalid'); }); }); describe('the event value is not present in the event tags', function() { it('should return null', function() { - var parsedEventValue = eventTagUtils.getEventValue({ - not_value: '13.37', - }, mockLogger); + var parsedEventValue = eventTagUtils.getEventValue( + { + not_value: '13.37', + }, + mockLogger + ); assert.strictEqual(parsedEventValue, null); }); diff --git a/lib/utils/event_tag_utils/index.ts b/lib/utils/event_tag_utils/index.ts new file mode 100644 index 000000000..d50292a39 --- /dev/null +++ b/lib/utils/event_tag_utils/index.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2017, 2019-2020, 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + FAILED_TO_PARSE_REVENUE, + FAILED_TO_PARSE_VALUE, + PARSED_NUMERIC_VALUE, + PARSED_REVENUE_VALUE, +} from 'log_message'; +import { LoggerFacade } from '../../logging/logger'; + +import { RESERVED_EVENT_KEYWORDS } from '../enums'; +import { EventTags } from '../../shared_types'; + +/** + * Provides utility method for parsing event tag values + */ +const REVENUE_EVENT_METRIC_NAME = RESERVED_EVENT_KEYWORDS.REVENUE; +const VALUE_EVENT_METRIC_NAME = RESERVED_EVENT_KEYWORDS.VALUE; + +/** + * Grab the revenue value from the event tags. "revenue" is a reserved keyword. + * @param {EventTags} eventTags + * @param {LoggerFacade} logger + * @return {number|null} + */ +export function getRevenueValue(eventTags: EventTags, logger?: LoggerFacade): number | null { + const rawValue = eventTags[REVENUE_EVENT_METRIC_NAME]; + + if (rawValue == null) { + // null or undefined event values + return null; + } + + const parsedRevenueValue = typeof rawValue === 'string' ? parseInt(rawValue) : Math.trunc(rawValue); + + if (isFinite(parsedRevenueValue)) { + logger?.info(PARSED_REVENUE_VALUE, parsedRevenueValue); + return parsedRevenueValue; + } else { + // NaN, +/- infinity values + logger?.info(FAILED_TO_PARSE_REVENUE, rawValue); + return null; + } +} + +/** + * Grab the event value from the event tags. "value" is a reserved keyword. + * @param {EventTags} eventTags + * @param {LoggerFacade} logger + * @return {number|null} + */ +export function getEventValue(eventTags: EventTags, logger?: LoggerFacade): number | null { + const rawValue = eventTags[VALUE_EVENT_METRIC_NAME]; + + if (rawValue == null) { + // null or undefined event values + return null; + } + + const parsedEventValue = typeof rawValue === 'string' ? parseFloat(rawValue) : rawValue; + + if (isFinite(parsedEventValue)) { + logger?.info(PARSED_NUMERIC_VALUE, parsedEventValue); + return parsedEventValue; + } else { + // NaN, +/- infinity values + logger?.info(FAILED_TO_PARSE_VALUE, rawValue); + return null; + } +} diff --git a/lib/utils/event_tags_validator/index.spec.ts b/lib/utils/event_tags_validator/index.spec.ts new file mode 100644 index 000000000..1b372ff0a --- /dev/null +++ b/lib/utils/event_tags_validator/index.spec.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { validate } from '.'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { INVALID_EVENT_TAGS } from 'error_message'; + +describe('validate', () => { + it('should validate the given event tags if event tag is an object', () => { + expect(validate({ testAttribute: 'testValue' })).toBe(true); + }); + + it('should throw an error if event tags is an array', () => { + const eventTagsArray = ['notGonnaWork']; + + expect(() => validate(eventTagsArray)).toThrow(OptimizelyError) + + try { + validate(eventTagsArray); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_EVENT_TAGS); + } + }); + + it('should throw an error if event tags is null', () => { + expect(() => validate(null)).toThrow(OptimizelyError); + + try { + validate(null); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_EVENT_TAGS); + } + }); + + it('should throw an error if event tags is a function', () => { + function invalidInput() { + console.log('This is an invalid input!'); + } + expect(() => validate(invalidInput)).toThrow(OptimizelyError); + + try { + validate(invalidInput); + } catch(err) { + expect(err).toBeInstanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_EVENT_TAGS); + } + }); +}); diff --git a/packages/optimizely-sdk/lib/utils/event_tags_validator/index.tests.js b/lib/utils/event_tags_validator/index.tests.js similarity index 59% rename from packages/optimizely-sdk/lib/utils/event_tags_validator/index.tests.js rename to lib/utils/event_tags_validator/index.tests.js index bf775c9a0..c18585d50 100644 --- a/packages/optimizely-sdk/lib/utils/event_tags_validator/index.tests.js +++ b/lib/utils/event_tags_validator/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2017, Optimizely + * Copyright 2017, 2020, 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,40 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var chai = require('chai'); -var assert = chai.assert; -var sprintf = require('sprintf-js').sprintf; -var eventTagsValidator = require('./'); +import { assert } from 'chai'; -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; +import { validate } from './'; +import { INVALID_EVENT_TAGS } from 'error_message'; describe('lib/utils/event_tags_validator', function() { describe('APIs', function() { describe('validate', function() { it('should validate the given event tags if event tags is an object', function() { - assert.isTrue(eventTagsValidator.validate({testAttribute: 'testValue'})); + assert.isTrue(validate({ testAttribute: 'testValue' })); }); it('should throw an error if event tags is an array', function() { var eventTagsArray = ['notGonnaWork']; - assert.throws(function() { - eventTagsValidator.validate(eventTagsArray); - }, sprintf(ERROR_MESSAGES.INVALID_EVENT_TAGS, 'EVENT_TAGS_VALIDATOR')); + const ex = assert.throws(function() { + validate(eventTagsArray); + }); + assert.equal(ex.baseMessage, INVALID_EVENT_TAGS); }); it('should throw an error if event tags is null', function() { - assert.throws(function() { - eventTagsValidator.validate(null); - }, sprintf(ERROR_MESSAGES.INVALID_EVENT_TAGS, 'EVENT_TAGS_VALIDATOR')); + const ex = assert.throws(function() { + validate(null); + }) + assert.equal(ex.baseMessage, INVALID_EVENT_TAGS); }); it('should throw an error if event tags is a function', function() { function invalidInput() { console.log('This is an invalid input!'); } - assert.throws(function() { - eventTagsValidator.validate(invalidInput); - }, sprintf(ERROR_MESSAGES.INVALID_EVENT_TAGS, 'EVENT_TAGS_VALIDATOR')); + const ex = assert.throws(function() { + validate(invalidInput); + }); + assert.equal(ex.baseMessage, INVALID_EVENT_TAGS); }); }); }); diff --git a/packages/optimizely-sdk/lib/utils/event_tags_validator/index.js b/lib/utils/event_tags_validator/index.ts similarity index 51% rename from packages/optimizely-sdk/lib/utils/event_tags_validator/index.js rename to lib/utils/event_tags_validator/index.ts index f10177c9e..421321f69 100644 --- a/packages/optimizely-sdk/lib/utils/event_tags_validator/index.js +++ b/lib/utils/event_tags_validator/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2017, Optimizely + * Copyright 2017, 2020, 2022 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,24 +17,19 @@ /** * Provides utility method for validating that event tags user has provided are valid */ +import { OptimizelyError } from '../../error/optimizly_error'; +import { INVALID_EVENT_TAGS } from 'error_message'; -var sprintf = require('sprintf-js').sprintf; - -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; -var MODULE_NAME = 'EVENT_TAGS_VALIDATOR'; - -module.exports = { - /** - * Validates user's provided event tags - * @param {Object} event tags - * @return {boolean} True if event tags are valid - * @throws If event tags are not valid - */ - validate: function(eventTags) { - if (typeof eventTags === 'object' && !Array.isArray(eventTags) && eventTags !== null) { - return true; - } else { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EVENT_TAGS, MODULE_NAME)); - } - }, -}; +/** + * Validates user's provided event tags + * @param {unknown} eventTags + * @return {boolean} true if event tags are valid + * @throws If event tags are not valid + */ +export function validate(eventTags: unknown): boolean { + if (typeof eventTags === 'object' && !Array.isArray(eventTags) && eventTags !== null) { + return true; + } else { + throw new OptimizelyError(INVALID_EVENT_TAGS); + } +} diff --git a/lib/utils/executor/backoff_retry_runner.spec.ts b/lib/utils/executor/backoff_retry_runner.spec.ts new file mode 100644 index 000000000..a1dd1f3a3 --- /dev/null +++ b/lib/utils/executor/backoff_retry_runner.spec.ts @@ -0,0 +1,139 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { runWithRetry } from './backoff_retry_runner'; +import { advanceTimersByTime } from '../../tests/testUtils'; + +const exhaustMicrotasks = async (loop = 100) => { + for(let i = 0; i < loop; i++) { + await Promise.resolve(); + } +} + +describe('runWithRetry', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should return the result of the task if it succeeds in first try', async () => { + const task = async () => 1; + const { result } = runWithRetry(task); + expect(await result).toBe(1); + }); + + it('should retry the task if it fails', async () => { + let count = 0; + const task = async () => { + count++; + if (count === 1) { + throw new Error('error'); + } + return 1; + }; + const { result } = runWithRetry(task); + + await exhaustMicrotasks(); + await advanceTimersByTime(0); + + expect(await result).toBe(1); + }); + + it('should retry the task up to the maxRetries before failing', async () => { + let count = 0; + const task = async () => { + count++; + throw new Error('error'); + }; + const { result } = runWithRetry(task, undefined, 5); + + for(let i = 0; i < 5; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(0); + } + + try { + await result; + } catch (e) { + expect(count).toBe(6); + } + }); + + it('should retry idefinitely if maxRetries is undefined', async () => { + let count = 0; + const task = async () => { + count++; + if (count < 500) { + throw new Error('error'); + } + return 1; + }; + + const { result } = runWithRetry(task); + + for(let i = 0; i < 500; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(0); + } + expect(await result).toBe(1); + expect(count).toBe(500); + }); + + it('should use the backoff controller to delay retries', async () => { + const task = vi.fn().mockImplementation(async () => { + throw new Error('error'); + }); + + const delays = [7, 13, 19, 20, 27]; + + let backoffCount = 0; + const backoff = { + backoff: () => { + return delays[backoffCount++]; + }, + reset: () => {}, + }; + + const { result } = runWithRetry(task, backoff, 5); + result.catch(() => {}); + + expect(task).toHaveBeenCalledTimes(1); + + for(let i = 1; i <= 5; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(delays[i - 1] - 1); + expect(task).toHaveBeenCalledTimes(i); + await advanceTimersByTime(1); + expect(task).toHaveBeenCalledTimes(i + 1); + } + }); + + it('should cancel the retry if the cancel function is called', async () => { + let count = 0; + const task = async () => { + count++; + throw new Error('error'); + }; + + const { result, cancelRetry } = runWithRetry(task, undefined, 100); + + for(let i = 0; i < 5; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(0); + } + + cancelRetry(); + + for(let i = 0; i < 100; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(0); + } + + try { + await result; + } catch (e) { + expect(count).toBe(6); + } + }); +}); diff --git a/lib/utils/executor/backoff_retry_runner.ts b/lib/utils/executor/backoff_retry_runner.ts new file mode 100644 index 000000000..f0b185a99 --- /dev/null +++ b/lib/utils/executor/backoff_retry_runner.ts @@ -0,0 +1,54 @@ +import { OptimizelyError } from "../../error/optimizly_error"; +import { RETRY_CANCELLED } from "error_message"; +import { resolvablePromise, ResolvablePromise } from "../promise/resolvablePromise"; +import { BackoffController } from "../repeater/repeater"; +import { AsyncProducer, Fn } from "../type"; + +export type RunResult<T> = { + result: Promise<T>; + cancelRetry: Fn; +}; + +type CancelSignal = { + cancelled: boolean; +} + +const runTask = <T>( + task: AsyncProducer<T>, + returnPromise: ResolvablePromise<T>, + cancelSignal: CancelSignal, + backoff?: BackoffController, + retryRemaining?: number, +): void => { + task().then((res) => { + returnPromise.resolve(res); + }).catch((e) => { + if (retryRemaining === 0) { + returnPromise.reject(e); + return; + } + if (cancelSignal.cancelled) { + returnPromise.reject(new OptimizelyError(RETRY_CANCELLED)); + return; + } + const delay = backoff?.backoff() ?? 0; + setTimeout(() => { + retryRemaining = retryRemaining === undefined ? undefined : retryRemaining - 1; + runTask(task, returnPromise, cancelSignal, backoff, retryRemaining); + }, delay); + }); +} + +export const runWithRetry = <T>( + task: AsyncProducer<T>, + backoff?: BackoffController, + maxRetries?: number +): RunResult<T> => { + const returnPromise = resolvablePromise<T>(); + const cancelSignal = { cancelled: false }; + const cancelRetry = () => { + cancelSignal.cancelled = true; + } + runTask(task, returnPromise, cancelSignal, backoff, maxRetries); + return { cancelRetry, result: returnPromise.promise }; +} diff --git a/lib/utils/executor/serial_runner.spec.ts b/lib/utils/executor/serial_runner.spec.ts new file mode 100644 index 000000000..6456735a9 --- /dev/null +++ b/lib/utils/executor/serial_runner.spec.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect, beforeEach } from 'vitest'; + +import { SerialRunner } from './serial_runner'; +import { resolvablePromise } from '../promise/resolvablePromise'; +import { exhaustMicrotasks } from '../../tests/testUtils'; + +describe('SerialRunner', () => { + let serialRunner: SerialRunner; + + beforeEach(() => { + serialRunner = new SerialRunner(); + }); + + it('should return result from a single async function', async () => { + const fn = () => Promise.resolve('result'); + + const result = await serialRunner.run(fn); + + expect(result).toBe('result'); + }); + + it('should reject with same error when the passed function rejects', async () => { + const error = new Error('test error'); + const fn = () => Promise.reject(error); + + await expect(serialRunner.run(fn)).rejects.toThrow(error); + }); + + it('should execute multiple async functions in order', async () => { + const executionOrder: number[] = []; + const promises = [resolvablePromise(), resolvablePromise(), resolvablePromise()]; + + const createTask = (id: number) => async () => { + executionOrder.push(id); + await promises[id]; + return id; + }; + + const results = [serialRunner.run(createTask(0)), serialRunner.run(createTask(1)), serialRunner.run(createTask(2))]; + + // only first task should have started + await exhaustMicrotasks(); + expect(executionOrder).toEqual([0]); + + // Resolve first task - second should start + promises[0].resolve(''); + await exhaustMicrotasks(); + expect(executionOrder).toEqual([0, 1]); + + // Resolve second task - third should start + promises[1].resolve(''); + await exhaustMicrotasks(); + expect(executionOrder).toEqual([0, 1, 2]); + + // Resolve third task - all done + promises[2].resolve(''); + + // Verify all results are correct + expect(await results[0]).toBe(0); + expect(await results[1]).toBe(1); + expect(await results[2]).toBe(2); + }); + + it('should continue execution even if one function throws an error', async () => { + const executionOrder: number[] = []; + const promises = [resolvablePromise(), resolvablePromise(), resolvablePromise()]; + + const createTask = (id: number) => async () => { + executionOrder.push(id); + await promises[id]; + return id; + }; + + const results = [serialRunner.run(createTask(0)), serialRunner.run(createTask(1)), serialRunner.run(createTask(2))]; + + // only first task should have started + await exhaustMicrotasks(); + expect(executionOrder).toEqual([0]); + + // reject first task - second should still start + promises[0].reject(new Error('first error')); + await exhaustMicrotasks(); + expect(executionOrder).toEqual([0, 1]); + + // reject second task - third should still start + promises[1].reject(new Error('second error')); + await exhaustMicrotasks(); + expect(executionOrder).toEqual([0, 1, 2]); + + // Resolve third task - all done + promises[2].resolve(''); + + // Verify results - first and third succeed, second fails + await expect(results[0]).rejects.toThrow('first error'); + await expect(results[1]).rejects.toThrow('second error'); + await expect(results[2]).resolves.toBe(2); + }); + + it('should handle functions that return different types', async () => { + const numberFn = () => Promise.resolve(42); + const stringFn = () => Promise.resolve('hello'); + const objectFn = () => Promise.resolve({ key: 'value' }); + const arrayFn = () => Promise.resolve([1, 2, 3]); + const booleanFn = () => Promise.resolve(true); + const nullFn = () => Promise.resolve(null); + const undefinedFn = () => Promise.resolve(undefined); + + const results = await Promise.all([ + serialRunner.run(numberFn), + serialRunner.run(stringFn), + serialRunner.run(objectFn), + serialRunner.run(arrayFn), + serialRunner.run(booleanFn), + serialRunner.run(nullFn), + serialRunner.run(undefinedFn), + ]); + + expect(results).toEqual([42, 'hello', { key: 'value' }, [1, 2, 3], true, null, undefined]); + }); + + it('should handle empty function that returns undefined', async () => { + const emptyFn = () => Promise.resolve(undefined); + + const result = await serialRunner.run(emptyFn); + + expect(result).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/lib/utils/executor/serial_runner.ts b/lib/utils/executor/serial_runner.ts new file mode 100644 index 000000000..243cae0b1 --- /dev/null +++ b/lib/utils/executor/serial_runner.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AsyncProducer } from "../type"; + +class SerialRunner { + private waitPromise: Promise<unknown> = Promise.resolve(); + + // each call to serialize adds a new function to the end of the promise chain + // the function is called when the previous promise resolves + // if the function throws, the error is caught and ignored to allow the chain to continue + // the result of the function is returned as a promise + // if multiple calls to serialize are made, they will be executed in order + // even if some of them throw errors + + run<T>(fn: AsyncProducer<T>): Promise<T> { + const resultPromise = this.waitPromise.then(fn); + this.waitPromise = resultPromise.catch(() => {}); + return resultPromise; + } +} + +export { SerialRunner }; diff --git a/packages/utils/__tests__/utils.spec.ts b/lib/utils/fns/index.spec.ts similarity index 54% rename from packages/utils/__tests__/utils.spec.ts rename to lib/utils/fns/index.spec.ts index 8d2a2a1f2..1eec0d746 100644 --- a/packages/utils/__tests__/utils.spec.ts +++ b/lib/utils/fns/index.spec.ts @@ -1,23 +1,8 @@ -/// <reference types="jest" /> -import { isValidEnum, groupBy, objectValues, find, keyBy, sprintf } from '../src' +import { describe, it, expect } from 'vitest'; -describe('utils', () => { - describe('isValidEnum', () => { - enum myEnum { - FOO = 0, - BAR = 1, - } - - it('should return false when not valid', () => { - expect(isValidEnum(myEnum, 2)).toBe(false) - }) - - it('should return true when valid', () => { - expect(isValidEnum(myEnum, 1)).toBe(true) - expect(isValidEnum(myEnum, myEnum.FOO)).toBe(true) - }) - }) +import { groupBy, objectEntries, objectValues, find, sprintf, keyBy, assignBy } from '.' +describe('utils', () => { describe('groupBy', () => { it('should group values by some key function', () => { const input = [ @@ -37,6 +22,12 @@ describe('utils', () => { }) }) + describe('objectEntries', () => { + it('should return object entries', () => { + expect(objectEntries({ foo: 'bar', bar: 123 })).toEqual([['foo', 'bar'], ['bar', 123]]) + }) + }) + describe('objectValues', () => { it('should return object values', () => { expect(objectValues({ foo: 'bar', bar: 123 })).toEqual(['bar', 123]) @@ -77,7 +68,7 @@ describe('utils', () => { { key: 'baz', firstName: 'james', lastName: 'foxy' }, ] - expect(keyBy(input, item => item.key)).toEqual({ + expect(keyBy(input, 'key')).toEqual({ foo: { key: 'foo', firstName: 'jordan', lastName: 'foo' }, bar: { key: 'bar', firstName: 'jordan', lastName: 'bar' }, baz: { key: 'baz', firstName: 'james', lastName: 'foxy' }, @@ -85,6 +76,78 @@ describe('utils', () => { }) }) + describe('assignBy', () => { + it('should assign array elements to an object using the specified key', () => { + const input = [ + { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + { key: 'baz', firstName: 'james', lastName: 'foxy' }, + ] + const base = {} + + assignBy(input, 'key', base) + + expect(base).toEqual({ + foo: { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + bar: { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + baz: { key: 'baz', firstName: 'james', lastName: 'foxy' }, + }) + }) + + it('should append to an existing object', () => { + const input = [ + { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + ] + const base: any = { existing: 'value' } + + assignBy(input, 'key', base) + + expect(base).toEqual({ + existing: 'value', + foo: { key: 'foo', firstName: 'jordan', lastName: 'foo' }, + bar: { key: 'bar', firstName: 'jordan', lastName: 'bar' }, + }) + }) + + it('should handle empty array', () => { + const base: any = { existing: 'value' } + + assignBy([], 'key', base) + + expect(base).toEqual({ existing: 'value' }) + }) + + it('should handle null/undefined array', () => { + const base: any = { existing: 'value' } + + assignBy(null as any, 'key', base) + expect(base).toEqual({ existing: 'value' }) + + assignBy(undefined as any, 'key', base) + expect(base).toEqual({ existing: 'value' }) + }) + + it('should override existing values with the same key', () => { + const input = [ + { key: 'foo', firstName: 'jordan', lastName: 'updated' }, + { key: 'bar', firstName: 'james', lastName: 'new' }, + ] + const base: any = { + foo: { key: 'foo', firstName: 'john', lastName: 'original' }, + existing: 'value' + } + + assignBy(input, 'key', base) + + expect(base).toEqual({ + existing: 'value', + foo: { key: 'foo', firstName: 'jordan', lastName: 'updated' }, + bar: { key: 'bar', firstName: 'james', lastName: 'new' }, + }) + }) + }) + describe('sprintf', () => { it('sprintf(msg)', () => { expect(sprintf('this is my message')).toBe('this is my message') diff --git a/lib/utils/fns/index.tests.js b/lib/utils/fns/index.tests.js new file mode 100644 index 000000000..f2be54fea --- /dev/null +++ b/lib/utils/fns/index.tests.js @@ -0,0 +1,88 @@ +/** + * Copyright 2019-2021 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; + +import fns from './'; + +describe('lib/utils/fns', function() { + describe('APIs', function() { + describe('isFinite', function() { + it('should return false for invalid numbers', function() { + assert.isFalse(fns.isSafeInteger(Infinity)); + assert.isFalse(fns.isSafeInteger(-Infinity)); + assert.isFalse(fns.isSafeInteger(NaN)); + assert.isFalse(fns.isSafeInteger(undefined)); + assert.isFalse(fns.isSafeInteger('3')); + assert.isFalse(fns.isSafeInteger(Math.pow(2, 53) + 2)); + assert.isFalse(fns.isSafeInteger(-Math.pow(2, 53) - 2)); + }); + + it('should return true for valid numbers', function() { + assert.isTrue(fns.isSafeInteger(0)); + assert.isTrue(fns.isSafeInteger(10)); + assert.isTrue(fns.isSafeInteger(10.5)); + assert.isTrue(fns.isSafeInteger(Math.pow(2, 53))); + assert.isTrue(fns.isSafeInteger(-Math.pow(2, 53))); + }); + }); + + describe('keyBy', function() { + it('should return correct object when a key is provided', function() { + var arr = [ + { key1: 'row1', key2: 'key2row1' }, + { key1: 'row2', key2: 'key2row2' }, + { key1: 'row3', key2: 'key2row3' }, + { key1: 'row4', key2: 'key2row4' }, + ]; + + var obj = fns.keyBy(arr, 'key1'); + + assert.deepEqual(obj, { + row1: { key1: 'row1', key2: 'key2row1' }, + row2: { key1: 'row2', key2: 'key2row2' }, + row3: { key1: 'row3', key2: 'key2row3' }, + row4: { key1: 'row4', key2: 'key2row4' }, + }); + }); + + it('should return empty object when first argument is null or undefined', function() { + var obj = fns.keyBy(null, 'key1'); + assert.isEmpty(obj); + + obj = fns.keyBy(undefined, 'key1'); + assert.isEmpty(obj); + }); + }); + + describe('isNumber', function() { + it('should return true in case of number', function() { + assert.isTrue(fns.isNumber(3)); + }); + it('should return true in case of value from Number object ', function() { + assert.isTrue(fns.isNumber(Number.MIN_VALUE)); + }); + it('should return true in case of Infinity ', function() { + assert.isTrue(fns.isNumber(Infinity)); + }); + it('should return false in case of string', function() { + assert.isFalse(fns.isNumber('3')); + }); + it('should return false in case of null', function() { + assert.isFalse(fns.isNumber(null)); + }); + }); + }); +}); diff --git a/lib/utils/fns/index.ts b/lib/utils/fns/index.ts new file mode 100644 index 000000000..5b07b3aad --- /dev/null +++ b/lib/utils/fns/index.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2017, 2019-2020, 2022-2023, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { v4 } from 'uuid'; + +const MAX_SAFE_INTEGER_LIMIT = Math.pow(2, 53); + +export function currentTimestamp(): number { + return Math.round(new Date().getTime()); +} + +export function isSafeInteger(number: unknown): boolean { + return typeof number == 'number' && Math.abs(number) <= MAX_SAFE_INTEGER_LIMIT; +} + +export function keyBy<K>(arr: K[], key: string): Record<string, K> { + if (!arr) return {}; + + const base: Record<string, K> = {}; + assignBy(arr, key, base); + return base; +} + +export function assignBy<K>(arr: K[], key: string, base: Record<string, K>): void { + if (!arr) return; + + arr.forEach((e) => { + base[(e as any)[key]] = e; + }); +} + + +function isNumber(value: unknown): boolean { + return typeof value === 'number'; +} + +export function uuid(): string { + return v4(); +} + +export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>; + +export function getTimestamp(): number { + return new Date().getTime(); +} + +export function groupBy<K>(arr: K[], grouperFn: (item: K) => string): Array<K[]> { + const grouper: { [key: string]: K[] } = {}; + + arr.forEach(item => { + const key = grouperFn(item); + grouper[key] = grouper[key] || []; + grouper[key].push(item); + }); + + return objectValues(grouper); +} + +export function objectValues<K>(obj: { [key: string]: K }): K[] { + return Object.keys(obj).map(key => obj[key]); +} + +export function objectEntries<K>(obj: { [key: string]: K }): [string, K][] { + return Object.keys(obj).map(key => [key, obj[key]]); +} + +export function find<K>(arr: K[], cond: (arg: K) => boolean): K | undefined { + let found; + + for (const item of arr) { + if (cond(item)) { + found = item; + break; + } + } + + return found; +} + +// TODO[OASIS-6649]: Don't use any type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function sprintf(format: string, ...args: any[]): string { + let i = 0; + return format.replace(/%s/g, function() { + const arg = args[i++]; + const type = typeof arg; + if (type === 'function') { + return arg(); + } else if (type === 'string') { + return arg; + } else { + return String(arg); + } + }); +} + +/** + * Checks two string arrays for equality. + * @param arrayA First Array to be compared against. + * @param arrayB Second Array to be compared against. + * @returns {boolean} True if both arrays are equal, otherwise returns false. + */ +export function checkArrayEquality(arrayA: string[], arrayB: string[]): boolean { + return arrayA.length === arrayB.length && arrayA.every((item, index) => item === arrayB[index]); +} + +export default { + checkArrayEquality, + currentTimestamp, + isSafeInteger, + keyBy, + uuid, + isNumber, + getTimestamp, + groupBy, + objectValues, + objectEntries, + find, + sprintf, +}; diff --git a/lib/utils/http_request_handler/http.ts b/lib/utils/http_request_handler/http.ts new file mode 100644 index 000000000..ca7e63ae3 --- /dev/null +++ b/lib/utils/http_request_handler/http.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2019-2020, 2022, 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * List of key-value pairs to be used in an HTTP requests + */ +export interface Headers { + [header: string]: string | undefined; +} + +/** + * Simplified Response object containing only needed information + */ +export interface Response { + statusCode: number; + body: string; + headers: Headers; +} + +/** + * Cancellable request wrapper around a Promised response + */ +export interface AbortableRequest { + abort(): void; + responsePromise: Promise<Response>; +} + +export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'; + +/** + * Client that handles sending requests and receiving responses + */ +export interface RequestHandler { + makeRequest(requestUrl: string, headers: Headers, method: HttpMethod, data?: string): AbortableRequest; +} diff --git a/lib/utils/http_request_handler/http_util.ts b/lib/utils/http_request_handler/http_util.ts new file mode 100644 index 000000000..c38217a40 --- /dev/null +++ b/lib/utils/http_request_handler/http_util.ts @@ -0,0 +1,4 @@ + +export const isSuccessStatusCode = (statusCode: number): boolean => { + return statusCode >= 200 && statusCode < 400; +} diff --git a/lib/utils/http_request_handler/request_handler.browser.spec.ts b/lib/utils/http_request_handler/request_handler.browser.spec.ts new file mode 100644 index 000000000..68a8a5bb7 --- /dev/null +++ b/lib/utils/http_request_handler/request_handler.browser.spec.ts @@ -0,0 +1,144 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; + +import { FakeXMLHttpRequest, FakeXMLHttpRequestStatic, fakeXhr } from 'nise'; +import { BrowserRequestHandler } from './request_handler.browser'; +import { getMockLogger } from '../../tests/mock/mock_logger'; + +describe('BrowserRequestHandler', () => { + const host = 'https://endpoint.example.com/api/query'; + const body = '{"foo":"bar"}'; + const dateString = 'Fri, 08 Mar 2019 18:57:18 GMT'; + + describe('makeRequest', () => { + let mockXHR: FakeXMLHttpRequestStatic; + let xhrs: FakeXMLHttpRequest[]; + let browserRequestHandler: BrowserRequestHandler; + + beforeEach(() => { + xhrs = []; + mockXHR = fakeXhr.useFakeXMLHttpRequest(); + mockXHR.onCreate = (request): number => xhrs.push(request); + browserRequestHandler = new BrowserRequestHandler({ logger: getMockLogger() }); + }); + + afterEach(() => { + mockXHR.restore(); + }); + + it('should make a GET request to the argument URL', async () => { + const request = browserRequestHandler.makeRequest(host, {}, 'get'); + + expect(xhrs.length).toBe(1); + const xhr = xhrs[0]; + const { url, method } = xhr; + expect({ url, method }).toEqual({ + url: host, + method: 'get', + }); + xhr.respond(200, {}, body); + + const response = await request.responsePromise; + + expect(response.body).toEqual(body); + }); + + it('should return a 200 response', async () => { + const request = browserRequestHandler.makeRequest(host, {}, 'get'); + + const xhr = xhrs[0]; + xhr.respond(200, {}, body); + + const response = await request.responsePromise; + expect(response).toEqual({ + statusCode: 200, + headers: {}, + body, + }); + }); + + it('should return a 404 response', async () => { + const request = browserRequestHandler.makeRequest(host, {}, 'get'); + + const xhr = xhrs[0]; + xhr.respond(404, {}, ''); + + const response = await request.responsePromise; + expect(response).toEqual({ + statusCode: 404, + headers: {}, + body: '', + }); + }); + + it('should include headers from the headers argument in the request', async () => { + const request = browserRequestHandler.makeRequest(host, { + 'if-modified-since': dateString, + }, 'get'); + + expect(xhrs.length).toBe(1); + expect(xhrs[0].requestHeaders['if-modified-since']).toBe(dateString); + + xhrs[0].respond(404, {}, ''); + + await request.responsePromise; + }); + + it('should include headers from the response in the eventual response in the return value', async () => { + const request = browserRequestHandler.makeRequest(host, {}, 'get'); + const xhr = xhrs[0]; + xhr.respond( + 200, + { + 'content-type': 'application/json', + 'last-modified': dateString, + }, + body, + ); + + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 200, + body, + headers: { + 'content-type': 'application/json', + 'last-modified': dateString, + }, + }); + }); + + it('should return a rejected promise when there is a request error', async () => { + const request = browserRequestHandler.makeRequest(host, {}, 'get'); + xhrs[0].error(); + + await expect(request.responsePromise).rejects.toThrow(); + }); + + it('should set a timeout on the request object', () => { + const timeout = 60000; + const onCreateMock = vi.fn(); + mockXHR.onCreate = onCreateMock; + + new BrowserRequestHandler({ logger: getMockLogger(), timeout }).makeRequest(host, {}, 'get'); + + expect(onCreateMock).toBeCalledTimes(1); + expect(onCreateMock.mock.calls[0][0].timeout).toBe(timeout); + }); + }); +}); diff --git a/lib/utils/http_request_handler/request_handler.browser.ts b/lib/utils/http_request_handler/request_handler.browser.ts new file mode 100644 index 000000000..340dcca33 --- /dev/null +++ b/lib/utils/http_request_handler/request_handler.browser.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2022, 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbortableRequest, Headers, RequestHandler, Response } from './http'; +import { LoggerFacade, LogLevel } from '../../logging/logger'; +import { REQUEST_TIMEOUT_MS } from '../enums'; +import { REQUEST_ERROR, REQUEST_TIMEOUT, UNABLE_TO_PARSE_AND_SKIPPED_HEADER } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +/** + * Handles sending requests and receiving responses over HTTP via XMLHttpRequest + */ +export class BrowserRequestHandler implements RequestHandler { + private logger?: LoggerFacade; + private timeout: number; + + public constructor(opt: { logger?: LoggerFacade, timeout?: number } = {}) { + this.logger = opt.logger; + this.timeout = opt.timeout ?? REQUEST_TIMEOUT_MS; + } + + /** + * Builds an XMLHttpRequest + * @param requestUrl Fully-qualified URL to which to send the request + * @param headers List of headers to include in the request + * @param method HTTP method to use + * @param data?? stringified version of data to POST, PUT, etc + * @returns AbortableRequest contains both the response Promise and capability to abort() + */ + public makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { + const request = new XMLHttpRequest(); + + const responsePromise: Promise<Response> = new Promise((resolve, reject) => { + request.open(method, requestUrl, true); + + this.setHeadersInXhr(headers, request); + + request.onreadystatechange = (): void => { + if (request.readyState === XMLHttpRequest.DONE) { + const statusCode = request.status; + if (statusCode === 0) { + reject(new OptimizelyError(REQUEST_ERROR)); + return; + } + + const headers = this.parseHeadersFromXhr(request); + const response: Response = { + statusCode: request.status, + body: request.responseText, + headers, + }; + resolve(response); + } + }; + + request.timeout = this.timeout; + + request.ontimeout = (): void => { + this.logger?.warn(REQUEST_TIMEOUT); + }; + + request.send(data); + }); + + return { + responsePromise, + abort(): void { + request.abort(); + }, + }; + } + + /** + * Sets the header collection for an XHR + * @param headers Headers to set + * @param request Request into which headers are to be set + * @private + */ + private setHeadersInXhr(headers: Headers, request: XMLHttpRequest): void { + Object.keys(headers).forEach(headerName => { + const header = headers[headerName]; + if (typeof header === 'string') { + request.setRequestHeader(headerName, header); + } + }); + } + + /** + * Parses headers from an XHR + * @param request Request containing headers to be retrieved + * @private + * @returns List of headers without duplicates + */ + private parseHeadersFromXhr(request: XMLHttpRequest): Headers { + const allHeadersString = request.getAllResponseHeaders(); + + if (allHeadersString === null) { + return {}; + } + + const headerLines = allHeadersString.split('\r\n'); + const headers: Headers = {}; + headerLines.forEach(headerLine => { + try { + const separatorIndex = headerLine.indexOf(': '); + if (separatorIndex > -1) { + const headerName = headerLine.slice(0, separatorIndex); + const headerValue = headerLine.slice(separatorIndex + 2); + if (headerName && headerValue) { + headers[headerName] = headerValue; + } + } + } catch { + this.logger?.warn(UNABLE_TO_PARSE_AND_SKIPPED_HEADER, headerLine); + } + }); + return headers; + } +} diff --git a/lib/utils/http_request_handler/request_handler.node.spec.ts b/lib/utils/http_request_handler/request_handler.node.spec.ts new file mode 100644 index 000000000..1865df88b --- /dev/null +++ b/lib/utils/http_request_handler/request_handler.node.spec.ts @@ -0,0 +1,237 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, beforeEach, afterEach, beforeAll, afterAll, it, vi, expect } from 'vitest'; + +import nock from 'nock'; +import zlib from 'zlib'; +import { NodeRequestHandler } from './request_handler.node'; +import { getMockLogger } from '../../tests/mock/mock_logger'; + +beforeAll(() => { + nock.disableNetConnect(); +}); + +afterAll(() => { + nock.enableNetConnect(); +}); + +describe('NodeRequestHandler', () => { + const host = 'https://endpoint.example.com'; + const path = '/api/query'; + const body = '{"foo":"bar"}'; + + let nodeRequestHandler: NodeRequestHandler; + + beforeEach(() => { + nodeRequestHandler = new NodeRequestHandler({ logger: getMockLogger() }); + }); + + afterEach(async () => { + nock.cleanAll(); + }); + + describe('makeRequest', () => { + it('should handle a 200 response back from a post', async () => { + const scope = nock(host) + .post(path) + .reply(200, body); + + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'post', body); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 200, + body, + headers: {}, + }); + scope.done(); + }); + + it('should handle a 400 response back ', async () => { + const scope = nock(host) + .post(path) + .reply(400, ''); + + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'post'); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 400, + body: '', + headers: {}, + }); + scope.done(); + }); + + it('should include headers from the headers argument in the request', async () => { + const scope = nock(host) + .matchHeader('if-modified-since', 'Fri, 08 Mar 2019 18:57:18 GMT') + .get(path) + .reply(304, ''); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, { + 'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT', + }, 'get'); + const response = await request.responsePromise; + expect(response).toEqual({ + statusCode: 304, + body: '', + headers: {}, + }); + scope.done(); + }); + + it('should add Accept-Encoding request header and unzips a gzipped response body', async () => { + const scope = nock(host) + .matchHeader('accept-encoding', 'gzip,deflate') + .get(path) + .reply(200, () => zlib.gzipSync(body), { 'content-encoding': 'gzip' }); + + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'get'); + const response = await request.responsePromise; + + expect(response).toMatchObject({ + statusCode: 200, + body: body, + }); + scope.done(); + }); + + it('should include headers from the response in the eventual response in the return value', async () => { + const scope = nock(host) + .get(path) + .reply( + 200, + JSON.parse(body), + { + 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', + }, + ); + + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'get'); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 200, + body, + headers: { + 'content-type': 'application/json', + 'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT', + }, + }); + scope.done(); + }); + + it('should handle a URL with a query string', async () => { + const pathWithQuery = '/datafiles/123.json?from_my_app=true'; + const scope = nock(host) + .get(pathWithQuery) + .reply(200, JSON.parse(body)); + + const request = nodeRequestHandler.makeRequest(`${host}${pathWithQuery}`, {}, 'get'); + await request.responsePromise; + + scope.done(); + }); + + it('should throw error for a URL with http protocol (not https)', async () => { + const invalidHttpProtocolUrl = 'http://some.example.com'; + + const request = nodeRequestHandler.makeRequest(invalidHttpProtocolUrl, {}, 'get'); + + await expect(request.responsePromise).rejects.toThrow(); + }); + + it('should returns a rejected response promise when the URL protocol is unsupported', async () => { + const invalidProtocolUrl = 'ftp://something/datafiles/123.json'; + + const request = nodeRequestHandler.makeRequest(invalidProtocolUrl, {}, 'get'); + + await expect(request.responsePromise).rejects.toThrow(); + }); + + it('should return a rejected promise when there is a request error', async () => { + const scope = nock(host) + .get(path) + .replyWithError({ + message: 'Connection error', + code: 'CONNECTION_ERROR', + }); + const request = nodeRequestHandler.makeRequest(`${host}${path}`, {}, 'get'); + await expect(request.responsePromise).rejects.toThrow(); + scope.done(); + }); + + it('should handle a url with a host and a port', async () => { + const hostWithPort = 'https://datafiles:44311'; + const path = '/12/345.json'; + const scope = nock(hostWithPort) + .get(path) + .reply(200, body); + + const request = nodeRequestHandler.makeRequest(`${hostWithPort}${path}`, {}, 'get'); + const response = await request.responsePromise; + + expect(response).toEqual({ + statusCode: 200, + body, + headers: {}, + }); + scope.done(); + }); + + describe('timeout', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + it('should reject the response promise and abort the request when the response is not received before the timeout', async () => { + const scope = nock(host) + .get(path) + .delay({ head: 2000, body: 2000 }) + .reply(200, body); + + const abortEventListener = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let emittedReq: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const requestListener = (request: any): void => { + emittedReq = request; + emittedReq.once('timeout', abortEventListener); + }; + scope.on('request', requestListener); + + const request = new NodeRequestHandler({ logger: getMockLogger(), timeout: 100 }).makeRequest(`${host}${path}`, {}, 'get'); + + vi.advanceTimersByTime(60000); + vi.runAllTimers(); // <- explicitly tell vi to run all setTimeout, setInterval + vi.runAllTicks(); // <- explicitly tell vi to run all Promise callback + await expect(request.responsePromise).rejects.toThrow(); + expect(abortEventListener).toBeCalledTimes(1); + + scope.done(); + if (emittedReq) { + emittedReq.off('timeout', abortEventListener); + } + scope.off('request', requestListener); + }); + }); + }); +}); diff --git a/lib/utils/http_request_handler/request_handler.node.ts b/lib/utils/http_request_handler/request_handler.node.ts new file mode 100644 index 000000000..16af94caf --- /dev/null +++ b/lib/utils/http_request_handler/request_handler.node.ts @@ -0,0 +1,185 @@ +/** + * Copyright 2022-2023 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import http from 'http'; +import https from 'https'; +import url from 'url'; +import { AbortableRequest, Headers, RequestHandler, Response } from './http'; +import decompressResponse from 'decompress-response'; +import { LoggerFacade } from '../../logging/logger'; +import { REQUEST_TIMEOUT_MS } from '../enums'; +import { NO_STATUS_CODE_IN_RESPONSE, REQUEST_ERROR, REQUEST_TIMEOUT, UNSUPPORTED_PROTOCOL } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +/** + * Handles sending requests and receiving responses over HTTP via NodeJS http module + */ +export class NodeRequestHandler implements RequestHandler { + private readonly logger?: LoggerFacade; + private readonly timeout: number; + + constructor(opt: { logger?: LoggerFacade; timeout?: number } = {}) { + this.logger = opt.logger; + this.timeout = opt.timeout ?? REQUEST_TIMEOUT_MS; + } + + /** + * Builds an XMLHttpRequest + * @param requestUrl Fully-qualified URL to which to send the request + * @param headers List of headers to include in the request + * @param method HTTP method to use + * @param data? stringified version of data to POST, PUT, etc + * @returns AbortableRequest contains both the response Promise and capability to abort() + */ + makeRequest(requestUrl: string, headers: Headers, method: string, data?: string): AbortableRequest { + const parsedUrl = url.parse(requestUrl); + + if (parsedUrl.protocol !== 'https:') { + return { + responsePromise: Promise.reject(new OptimizelyError(UNSUPPORTED_PROTOCOL, parsedUrl.protocol)), + abort: () => {}, + }; + } + + const request = https.request({ + ...this.getRequestOptionsFromUrl(parsedUrl), + method, + headers: { + ...headers, + 'accept-encoding': 'gzip,deflate', + 'content-length': String(data?.length || 0) + }, + timeout: this.timeout, + }); + const abortableRequest = this.getAbortableRequestFromRequest(request); + + if (data) { + request.write(data); + } + request.end(); + + return abortableRequest; + } + + /** + * Parses a URL into its constituent parts + * @param url URL object to parse + * @private + * @returns https.RequestOptions Standard request options dictionary + */ + private getRequestOptionsFromUrl(url: url.UrlWithStringQuery): https.RequestOptions { + return { + hostname: url.hostname, + path: url.path, + port: url.port, + protocol: url.protocol, + }; + } + + /** + * Parses headers from an http response + * @param incomingMessage Incoming response message to parse + * @private + * @returns Headers Dictionary of headers without duplicates + */ + private createHeadersFromNodeIncomingMessage(incomingMessage: http.IncomingMessage): Headers { + const headers: Headers = {}; + Object.keys(incomingMessage.headers).forEach(headerName => { + const headerValue = incomingMessage.headers[headerName]; + if (typeof headerValue === 'string') { + headers[headerName] = headerValue; + } else if (typeof headerValue === 'undefined') { + // no value provided for this header + } else { + // array + if (headerValue.length > 0) { + // We don't care about multiple values - just take the first one + headers[headerName] = headerValue[0]; + } + } + }); + return headers; + } + + /** + * Sends a built request handling response, errors, and events around the transmission + * @param request Request to send + * @private + * @returns AbortableRequest with simplified response promise + */ + private getAbortableRequestFromRequest(request: http.ClientRequest): AbortableRequest { + let aborted = false; + + const abort = () => { + aborted = true; + request.destroy(); + }; + + const responsePromise: Promise<Response> = new Promise((resolve, reject) => { + request.on('timeout', () => { + aborted = true; + request.destroy(); + reject(new OptimizelyError(REQUEST_TIMEOUT)); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request.on('error', (err: any) => { + if (err instanceof Error) { + reject(err); + } else if (typeof err === 'string') { + reject(new Error(err)); + } else { + reject(new OptimizelyError(REQUEST_ERROR)); + } + }); + + request.once('response', (incomingMessage: http.IncomingMessage) => { + if (aborted) { + return; + } + + const response = decompressResponse(incomingMessage); + + response.setEncoding('utf8'); + + let responseData = ''; + response.on('data', (chunk: string) => { + if (!aborted) { + responseData += chunk; + } + }); + + response.on('end', () => { + if (aborted) { + return; + } + + if (!incomingMessage.statusCode) { + reject(new OptimizelyError(NO_STATUS_CODE_IN_RESPONSE)); + return; + } + + resolve({ + statusCode: incomingMessage.statusCode, + body: responseData, + headers: this.createHeadersFromNodeIncomingMessage(incomingMessage), + }); + }); + }); + }); + + return { abort, responsePromise }; + } +} diff --git a/lib/utils/http_request_handler/request_handler_validator.ts b/lib/utils/http_request_handler/request_handler_validator.ts new file mode 100644 index 000000000..a9df4cc7c --- /dev/null +++ b/lib/utils/http_request_handler/request_handler_validator.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RequestHandler } from './http'; + +export const INVALID_REQUEST_HANDLER = 'Invalid request handler'; + +export const validateRequestHandler = (requestHandler: RequestHandler): void => { + if (!requestHandler || typeof requestHandler !== 'object') { + throw new Error(INVALID_REQUEST_HANDLER); + } + + if (typeof requestHandler.makeRequest !== 'function') { + throw new Error(INVALID_REQUEST_HANDLER); + } +} diff --git a/lib/utils/id_generator/index.ts b/lib/utils/id_generator/index.ts new file mode 100644 index 000000000..5f3c72387 --- /dev/null +++ b/lib/utils/id_generator/index.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2022-2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const idSuffixBase = 10_000; + +export class IdGenerator { + private idSuffixOffset = 0; + + // getId returns an Id that generally increases with each call. + // only exceptions are when idSuffix rotates back to 0 within the same millisecond + // or when the clock goes back + getId(): string { + const idSuffix = idSuffixBase + this.idSuffixOffset; + this.idSuffixOffset = (this.idSuffixOffset + 1) % idSuffixBase; + const timestamp = Date.now(); + return `${timestamp}${idSuffix}`; + } +} diff --git a/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts b/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts new file mode 100644 index 000000000..4a2fb77ed --- /dev/null +++ b/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { AsyncStorageStatic } from '@react-native-async-storage/async-storage' + +export const MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE = 'Module not found: @react-native-async-storage/async-storage'; + +export const getDefaultAsyncStorage = (): AsyncStorageStatic => { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('@react-native-async-storage/async-storage').default; + } catch (e) { + throw new Error(MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE); + } +}; diff --git a/lib/utils/json_schema_validator/index.spec.ts b/lib/utils/json_schema_validator/index.spec.ts new file mode 100644 index 000000000..20af5b51d --- /dev/null +++ b/lib/utils/json_schema_validator/index.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from 'vitest'; +import { validate } from '.'; +import testData from '../../tests/test_data'; +import { NO_JSON_PROVIDED, INVALID_DATAFILE } from 'error_message'; + +describe('validate', () => { + it('should throw an error if the object is not valid', () => { + expect(() => validate({})).toThrow(); + + try { + validate({}); + } catch (err) { + expect(err.baseMessage).toBe(INVALID_DATAFILE); + } + }); + + it('should throw an error if no json object is passed in', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => validate()).toThrow(); + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + validate(); + } catch (err) { + expect(err.baseMessage).toBe(NO_JSON_PROVIDED); + } + }); + + it('should validate specified Optimizely datafile', () => { + expect(validate(testData.getTestProjectConfig())).toBe(true); + }); +}); diff --git a/lib/utils/json_schema_validator/index.tests.js b/lib/utils/json_schema_validator/index.tests.js new file mode 100644 index 000000000..cace62047 --- /dev/null +++ b/lib/utils/json_schema_validator/index.tests.js @@ -0,0 +1,44 @@ +/** + * Copyright 2016-2020, 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; + +import { validate } from './'; +import testData from '../../tests/test_data'; +import { NO_JSON_PROVIDED } from 'error_message'; + + +describe('lib/utils/json_schema_validator', function() { + describe('APIs', function() { + describe('validate', function() { + it('should throw an error if the object is not valid', function() { + assert.throws(function() { + validate({}); + }); + }); + + it('should throw an error if no json object is passed in', function() { + const ex = assert.throws(function() { + validate(); + }); + assert.equal(ex.baseMessage, NO_JSON_PROVIDED); + }); + + it('should validate specified Optimizely datafile', function() { + assert.isTrue(validate(testData.getTestProjectConfig())); + }); + }); + }); +}); diff --git a/lib/utils/json_schema_validator/index.ts b/lib/utils/json_schema_validator/index.ts new file mode 100644 index 000000000..42fe19f11 --- /dev/null +++ b/lib/utils/json_schema_validator/index.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2016-2017, 2020, 2022, 2024 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema4, validate as jsonSchemaValidator } from 'json-schema'; + +import schema from '../../project_config/project_config_schema'; +import { INVALID_DATAFILE, INVALID_JSON, NO_JSON_PROVIDED } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +/** + * Validate the given json object against the specified schema + * @param {unknown} jsonObject The object to validate against the schema + * @param {JSONSchema4} validationSchema Provided schema to use for validation + * @param {boolean} shouldThrowOnError Should validation throw if invalid JSON object + * @return {boolean} true if the given object is valid; throws or false if invalid + */ +export function validate( + jsonObject: unknown, + validationSchema: JSONSchema4 = schema, + shouldThrowOnError = true +): boolean { + if (typeof jsonObject !== 'object' || jsonObject === null) { + throw new OptimizelyError(NO_JSON_PROVIDED); + } + + const result = jsonSchemaValidator(jsonObject, validationSchema); + if (result.valid) { + return true; + } + + if (!shouldThrowOnError) { + return false; + } + + if (Array.isArray(result.errors)) { + throw new OptimizelyError( + INVALID_DATAFILE, result.errors[0].property, result.errors[0].message + ); + } + + throw new OptimizelyError(INVALID_JSON); +} diff --git a/lib/utils/microtask/index.spec.ts b/lib/utils/microtask/index.spec.ts new file mode 100644 index 000000000..8d5fd9622 --- /dev/null +++ b/lib/utils/microtask/index.spec.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { scheduleMicrotask } from '.'; + +describe('scheduleMicrotask', () => { + it('should use queueMicrotask if available', async () => { + expect(typeof globalThis.queueMicrotask).toEqual('function'); + const cb = vi.fn(); + scheduleMicrotask(cb); + await Promise.resolve(); + expect(cb).toHaveBeenCalledTimes(1); + }); + + it('should polyfill if queueMicrotask is not available', async () => { + const originalQueueMicrotask = globalThis.queueMicrotask; + globalThis.queueMicrotask = undefined as any; // as any to pacify TS + + expect(globalThis.queueMicrotask).toBeUndefined(); + + const cb = vi.fn(); + scheduleMicrotask(cb); + await Promise.resolve(); + expect(cb).toHaveBeenCalledTimes(1); + + expect(globalThis.queueMicrotask).toBeUndefined(); + globalThis.queueMicrotask = originalQueueMicrotask; + }); +}); diff --git a/lib/utils/microtask/index.ts b/lib/utils/microtask/index.ts new file mode 100644 index 000000000..02e2c474e --- /dev/null +++ b/lib/utils/microtask/index.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type Callback = () => void; + +export const scheduleMicrotask = (callback: Callback): void => { + if (typeof queueMicrotask === 'function') { + queueMicrotask(callback); + } else { + Promise.resolve().then(callback); + } +} diff --git a/lib/utils/promise/operation_value.ts b/lib/utils/promise/operation_value.ts new file mode 100644 index 000000000..7f7aa3779 --- /dev/null +++ b/lib/utils/promise/operation_value.ts @@ -0,0 +1,50 @@ +import { PROMISE_NOT_ALLOWED } from '../../message/error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; +import { OpType, OpValue } from '../type'; + + +const isPromise = (val: any): boolean => { + return val && typeof val.then === 'function'; +} + +/** + * A class that wraps a value that can be either a synchronous value or a promise and provides + * a promise like interface. This class is used to handle both synchronous and asynchronous values + * in a uniform way. + */ +export class Value<OP extends OpType, V> { + constructor(public op: OP, public val: OpValue<OP, V>) {} + + get(): OpValue<OP, V> { + return this.val; + } + + then<NV>(fn: (v: V) => Value<OP, NV>): Value<OP, NV> { + if (this.op === 'sync') { + const newVal = fn(this.val as V); + return Value.of(this.op, newVal.get() as NV); + } + return Value.of(this.op, (this.val as Promise<V>).then(fn) as Promise<NV>); + } + + static all = <OP extends OpType, V>(op: OP, vals: Value<OP, V>[]): Value<OP, V[]> => { + if (op === 'sync') { + const values = vals.map(v => v.get() as V); + return Value.of(op, values); + } + + const promises = vals.map(v => v.get() as Promise<V>); + return Value.of(op, Promise.all(promises)); + } + + static of<OP extends OpType, V>(op: OP, val: V | Promise<V>): Value<OP, V> { + if (op === 'sync') { + if (isPromise(val)) { + throw new OptimizelyError(PROMISE_NOT_ALLOWED); + } + return new Value(op, val as OpValue<OP, V>); + } + + return new Value(op, Promise.resolve(val) as OpValue<OP, V>); + } +} diff --git a/lib/utils/promise/resolvablePromise.ts b/lib/utils/promise/resolvablePromise.ts new file mode 100644 index 000000000..354df2b7d --- /dev/null +++ b/lib/utils/promise/resolvablePromise.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const noop = () => {}; + +export type ResolvablePromise<T> = { + promise: Promise<T>; + resolve: (value: T | PromiseLike<T>) => void; + reject: (reason?: any) => void; + then: Promise<T>['then']; +}; + +export function resolvablePromise<T>(): ResolvablePromise<T> { + let resolve: (value: T | PromiseLike<T>) => void = noop; + let reject: (reason?: any) => void = noop; + const promise = new Promise<T>((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject, then: promise.then.bind(promise) }; +} diff --git a/lib/utils/repeater/repeater.spec.ts b/lib/utils/repeater/repeater.spec.ts new file mode 100644 index 000000000..e92594556 --- /dev/null +++ b/lib/utils/repeater/repeater.spec.ts @@ -0,0 +1,283 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, vi, it, beforeEach, afterEach, describe } from 'vitest'; +import { ExponentialBackoff, IntervalRepeater } from './repeater'; +import { advanceTimersByTime } from '../../tests/testUtils'; +import { resolvablePromise } from '../promise/resolvablePromise'; + +describe("ExponentialBackoff", () => { + it("should return the base with jitter on the first call", () => { + const exponentialBackoff = new ExponentialBackoff(5000, 10000, 1000); + const time = exponentialBackoff.backoff(); + + expect(time).toBeGreaterThanOrEqual(5000); + expect(time).toBeLessThanOrEqual(6000); + }); + + it('should use a random jitter within the specified limit', () => { + const exponentialBackoff1 = new ExponentialBackoff(5000, 10000, 1000); + const exponentialBackoff2 = new ExponentialBackoff(5000, 10000, 1000); + + const time = exponentialBackoff1.backoff(); + const time2 = exponentialBackoff2.backoff(); + + expect(time).toBeGreaterThanOrEqual(5000); + expect(time).toBeLessThanOrEqual(6000); + + expect(time2).toBeGreaterThanOrEqual(5000); + expect(time2).toBeLessThanOrEqual(6000); + + expect(time).not.toEqual(time2); + }); + + it("should double the time when backoff() is called", () => { + const exponentialBackoff = new ExponentialBackoff(5000, 20000, 1000); + const time = exponentialBackoff.backoff(); + + expect(time).toBeGreaterThanOrEqual(5000); + expect(time).toBeLessThanOrEqual(6000); + + const time2 = exponentialBackoff.backoff(); + expect(time2).toBeGreaterThanOrEqual(10000); + expect(time2).toBeLessThanOrEqual(11000); + + const time3 = exponentialBackoff.backoff(); + expect(time3).toBeGreaterThanOrEqual(20000); + expect(time3).toBeLessThanOrEqual(21000); + }); + + it('should not exceed the max time', () => { + const exponentialBackoff = new ExponentialBackoff(5000, 10000, 1000); + const time = exponentialBackoff.backoff(); + expect(time).toBeGreaterThanOrEqual(5000); + expect(time).toBeLessThanOrEqual(6000); + + const time2 = exponentialBackoff.backoff(); + expect(time2).toBeGreaterThanOrEqual(10000); + expect(time2).toBeLessThanOrEqual(11000); + + const time3 = exponentialBackoff.backoff(); + expect(time3).toBeGreaterThanOrEqual(10000); + expect(time3).toBeLessThanOrEqual(11000); + }); + + it('should reset the backoff time when reset() is called', () => { + const exponentialBackoff = new ExponentialBackoff(5000, 10000, 1000); + const time = exponentialBackoff.backoff(); + expect(time).toBeGreaterThanOrEqual(5000); + expect(time).toBeLessThanOrEqual(6000); + + exponentialBackoff.reset(); + const time2 = exponentialBackoff.backoff(); + expect(time2).toBeGreaterThanOrEqual(5000); + expect(time2).toBeLessThanOrEqual(6000); + }); +}); + + +describe("IntervalRepeater", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should call the handler at the specified interval', async() => { + const handler = vi.fn().mockResolvedValue(undefined); + + const intervalRepeater = new IntervalRepeater(2000); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(2); + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(3); + }); + + it('should call the handler with correct failureCount value', async() => { + const handler = vi.fn().mockRejectedValueOnce(new Error()) + .mockRejectedValueOnce(new Error()) + .mockRejectedValueOnce(new Error()) + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error()) + .mockResolvedValueOnce(undefined); + + const intervalRepeater = new IntervalRepeater(2000); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + expect(handler.mock.calls[0][0]).toBe(0); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(2); + expect(handler.mock.calls[1][0]).toBe(1); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(3); + expect(handler.mock.calls[2][0]).toBe(2); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(4); + expect(handler.mock.calls[3][0]).toBe(3); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(5); + expect(handler.mock.calls[4][0]).toBe(0); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(6); + expect(handler.mock.calls[5][0]).toBe(1); + }); + + it('should backoff when the handler fails if backoffController is provided', async() => { + const handler = vi.fn().mockRejectedValue(new Error()); + + const backoffController = { + backoff: vi.fn().mockReturnValue(1100), + reset: vi.fn(), + }; + + const intervalRepeater = new IntervalRepeater(30000, backoffController); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(30000); + expect(handler).toHaveBeenCalledTimes(1); + expect(backoffController.backoff).toHaveBeenCalledTimes(1); + + await advanceTimersByTime(1100); + expect(handler).toHaveBeenCalledTimes(2); + expect(backoffController.backoff).toHaveBeenCalledTimes(2); + + await advanceTimersByTime(1100); + expect(handler).toHaveBeenCalledTimes(3); + expect(backoffController.backoff).toHaveBeenCalledTimes(3); + }); + + it('should use the regular interval when the handler fails if backoffController is not provided', async() => { + const handler = vi.fn().mockRejectedValue(new Error()); + + const intervalRepeater = new IntervalRepeater(30000); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(30000); + expect(handler).toHaveBeenCalledTimes(1); + + await advanceTimersByTime(10000); + expect(handler).toHaveBeenCalledTimes(1); + await advanceTimersByTime(20000); + expect(handler).toHaveBeenCalledTimes(2); + }); + + it('should reset the backoffController after handler success', async () => { + const handler = vi.fn().mockRejectedValueOnce(new Error) + .mockRejectedValueOnce(new Error()) + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(undefined); + + const backoffController = { + + backoff: vi.fn().mockReturnValue(1100), + reset: vi.fn(), + }; + + const intervalRepeater = new IntervalRepeater(30000, backoffController); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(30000); + expect(handler).toHaveBeenCalledTimes(1); + expect(backoffController.backoff).toHaveBeenCalledTimes(1); + + await advanceTimersByTime(1100); + expect(handler).toHaveBeenCalledTimes(2); + expect(backoffController.backoff).toHaveBeenCalledTimes(2); + + await advanceTimersByTime(1100); + expect(handler).toHaveBeenCalledTimes(3); + + expect(backoffController.backoff).toHaveBeenCalledTimes(2); // backoff should not be called again + expect(backoffController.reset).toHaveBeenCalledTimes(1); // reset should be called once + + await advanceTimersByTime(1100); + expect(handler).toHaveBeenCalledTimes(3); // handler should be called after 30000ms + await advanceTimersByTime(30000 - 1100); + expect(handler).toHaveBeenCalledTimes(4); // handler should be called after 30000ms + }); + + + it('should wait for handler promise to resolve before scheduling another tick', async() => { + const ret = resolvablePromise(); + const handler = vi.fn().mockReturnValue(ret); + + const intervalRepeater = new IntervalRepeater(2000); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + + // should not schedule another call cause promise is pending + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + + ret.resolve(undefined); + await ret.promise; + + // Advance the timers to the next tick + await advanceTimersByTime(2000); + // The handler should be called again after the promise has resolved + expect(handler).toHaveBeenCalledTimes(2); + }); + + it('should not call the handler after stop is called', async() => { + const ret = resolvablePromise(); + const handler = vi.fn().mockReturnValue(ret); + + const intervalRepeater = new IntervalRepeater(2000); + intervalRepeater.setTask(handler); + + intervalRepeater.start(); + + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + + intervalRepeater.stop(); + + ret.resolve(undefined); + await ret.promise; + + await advanceTimersByTime(2000); + await advanceTimersByTime(2000); + expect(handler).toHaveBeenCalledTimes(1); + }); +}); diff --git a/lib/utils/repeater/repeater.ts b/lib/utils/repeater/repeater.ts new file mode 100644 index 000000000..9f307ab95 --- /dev/null +++ b/lib/utils/repeater/repeater.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AsyncTransformer } from "../type"; +import { scheduleMicrotask } from "../microtask"; + +// A repeater will invoke the task repeatedly. The time at which the task is invoked +// is determined by the implementation. +// The task is a function that takes a number as an argument and returns a promise. +// The number argument is the number of times the task previously failed consecutively. +// If the retuned promise resolves, the repeater will assume the task succeeded, +// and will reset the failure count. If the promise is rejected, the repeater will +// assume the task failed and will increase the current consecutive failure count. +export interface Repeater { + // If immediateExecution is true, the first exection of + // the task will be immediate but asynchronous. + start(immediateExecution?: boolean): void; + stop(): void; + reset(): void; + setTask(task: AsyncTransformer<number, unknown>): void; + isRunning(): boolean; +} + +export interface BackoffController { + backoff(): number; + reset(): void; +} + +export class ExponentialBackoff implements BackoffController { + private base: number; + private max: number; + private current: number; + private maxJitter: number; + + constructor(base: number, max: number, maxJitter: number) { + this.base = base; + this.max = max; + this.maxJitter = maxJitter; + this.current = base; + } + + backoff(): number { + const ret = this.current + this.maxJitter * Math.random(); + this.current = Math.min(this.current * 2, this.max); + return ret; + } + + reset(): void { + this.current = this.base; + } +} + +// IntervalRepeater is a Repeater that invokes the task at a fixed interval +// after the completion of the previous task invocation. If a backoff controller +// is provided, the repeater will use the backoff controller to determine the +// time between invocations after a failure instead. It will reset the backoffController +// on success. + +export class IntervalRepeater implements Repeater { + private timeoutId?: NodeJS.Timeout; + private task?: AsyncTransformer<number, void>; + private interval: number; + private failureCount = 0; + private backoffController?: BackoffController; + private running = false; + + constructor(interval: number, backoffController?: BackoffController) { + this.interval = interval; + this.backoffController = backoffController; + } + + isRunning(): boolean { + return this.running; + } + + private handleSuccess() { + this.failureCount = 0; + this.backoffController?.reset(); + this.setTimer(this.interval); + } + + private handleFailure() { + this.failureCount++; + const time = this.backoffController?.backoff() ?? this.interval; + this.setTimer(time); + } + + private setTimer(timeout: number) { + if (!this.running){ + return; + } + this.timeoutId = setTimeout(this.executeTask.bind(this), timeout); + } + + private executeTask() { + if (!this.task) { + return; + } + this.task(this.failureCount).then( + this.handleSuccess.bind(this), + this.handleFailure.bind(this) + ); + } + + start(immediateExecution?: boolean): void { + this.running = true; + if(immediateExecution) { + scheduleMicrotask(this.executeTask.bind(this)); + } else { + this.setTimer(this.interval); + } + } + + stop(): void { + this.running = false; + clearInterval(this.timeoutId); + } + + reset(): void { + this.failureCount = 0; + this.backoffController?.reset(); + this.stop(); + } + + setTask(task: AsyncTransformer<number, void>): void { + this.task = task; + } +} diff --git a/lib/utils/semantic_version/index.spec.ts b/lib/utils/semantic_version/index.spec.ts new file mode 100644 index 000000000..15dbbdbb9 --- /dev/null +++ b/lib/utils/semantic_version/index.spec.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from 'vitest'; +import * as semanticVersion from '.'; + +describe('compareVersion', () => { + it('should return 0 if user version and target version are equal', () => { + const versions = [ + ['2.0.1', '2.0.1'], + ['2.9.9-beta', '2.9.9-beta'], + ['2.1', '2.1.0'], + ['2', '2.12'], + ['2.9', '2.9.1'], + ['2.9+beta', '2.9+beta'], + ['2.9.9+beta', '2.9.9+beta'], + ['2.9.9+beta-alpha', '2.9.9+beta-alpha'], + ['2.2.3', '2.2.3+beta'], + ]; + + versions.forEach(([targetVersion, userVersion]) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(0); + }) + }); + + it('should return 1 when user version is greater than target version', () => { + const versions = [ + ['2.0.0', '2.0.1'], + ['2.0', '3.0.1'], + ['2.0.0', '2.1'], + ['2.1.2-beta', '2.1.2-release'], + ['2.1.3-beta1', '2.1.3-beta2'], + ['2.9.9-beta', '2.9.9'], + ['2.9.9+beta', '2.9.9'], + ['2.0.0', '2.1'], + ['3.7.0-prerelease+build', '3.7.0-prerelease+rc'], + ['2.2.3-beta-beta1', '2.2.3-beta-beta2'], + ['2.2.3-beta+beta1', '2.2.3-beta+beta2'], + ['2.2.3+beta2-beta1', '2.2.3+beta3-beta2'], + ['2.2.3+beta', '2.2.3'], + ]; + + versions.forEach(([targetVersion, userVersion]) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(1); + }) + }); + + it('should return -1 when user version is less than target version', () => { + const versions = [ + ['2.0.1', '2.0.0'], + ['3.0', '2.0.1'], + ['2.3', '2.0.1'], + ['2.3.5', '2.3.1'], + ['2.9.8', '2.9'], + ['3.1', '3'], + ['2.1.2-release', '2.1.2-beta'], + ['2.9.9+beta', '2.9.9-beta'], + ['3.7.0+build3.7.0-prerelease+build', '3.7.0-prerelease'], + ['2.1.3-beta-beta2', '2.1.3-beta'], + ['2.1.3-beta1+beta3', '2.1.3-beta1+beta2'], + ['2.1.3', '2.1.3-beta'], + ]; + + versions.forEach(([targetVersion, userVersion]) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(-1); + }) + }); + + it('should return null when user version is invalid', () => { + const versions = [ + '-', + '.', + '..', + '+', + '+test', + ' ', + '2 .3. 0', + '2.', + '.2.2', + '3.7.2.2', + '3.x', + ',', + '+build-prerelease', + '2..2', + ]; + const targetVersion = '2.1.0'; + + versions.forEach((userVersion) => { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + + expect(result).toBe(null); + }) + }); +}); diff --git a/lib/utils/semantic_version/index.tests.js b/lib/utils/semantic_version/index.tests.js new file mode 100644 index 000000000..2ec4f50f0 --- /dev/null +++ b/lib/utils/semantic_version/index.tests.js @@ -0,0 +1,93 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { assert } from 'chai'; +import * as semanticVersion from './'; + +describe('lib/utils/sematic_version', function() { + describe('APIs', function() { + describe('compareVersion', function() { + it('should return 0 if user version and target version are equal', function() { + const versions = [ + ['2.0.1', '2.0.1'], + ['2.9.9-beta', '2.9.9-beta'], + ['2.1', '2.1.0'], + ['2', '2.12'], + ['2.9', '2.9.1'], + ['2.9+beta', '2.9+beta'], + ['2.9.9+beta', '2.9.9+beta'], + ['2.9.9+beta-alpha', '2.9.9+beta-alpha'], + ['2.2.3', '2.2.3+beta'] + ]; + for (const [targetVersion, userVersion] of versions) { + const result = semanticVersion.compareVersion(targetVersion, userVersion) + assert.equal(result, 0, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + it('should return 1 when user version is greater than target version', function() { + const versions = [ + ['2.0.0', '2.0.1'], + ['2.0', '3.0.1'], + ['2.0.0', '2.1'], + ['2.1.2-beta', '2.1.2-release'], + ['2.1.3-beta1', '2.1.3-beta2'], + ['2.9.9-beta', '2.9.9'], + ['2.9.9+beta', '2.9.9'], + ['2.0.0', '2.1'], + ['3.7.0-prerelease+build', '3.7.0-prerelease+rc'], + ['2.2.3-beta-beta1', '2.2.3-beta-beta2'], + ['2.2.3-beta+beta1', '2.2.3-beta+beta2'], + ['2.2.3+beta2-beta1', '2.2.3+beta3-beta2'], + ['2.2.3+beta', '2.2.3'] + ]; + for (const [targetVersion, userVersion] of versions) { + const result = semanticVersion.compareVersion(targetVersion, userVersion) + assert.equal(result, 1, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return -1 when user version is less than target version', function() { + const versions = [ + ['2.0.1', '2.0.0'], + ['3.0', '2.0.1'], + ['2.3', '2.0.1'], + ['2.3.5', '2.3.1'], + ['2.9.8', '2.9'], + ['3.1', '3'], + ['2.1.2-release', '2.1.2-beta'], + ['2.9.9+beta', '2.9.9-beta'], + ['3.7.0+build3.7.0-prerelease+build', '3.7.0-prerelease'], + ['2.1.3-beta-beta2', '2.1.3-beta'], + ['2.1.3-beta1+beta3', '2.1.3-beta1+beta2'], + ['2.1.3', '2.1.3-beta'] + ]; + for (const [targetVersion, userVersion] of versions) { + const result = semanticVersion.compareVersion(targetVersion, userVersion) + assert.equal(result, -1, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + it('should return null when user version is invalid', function() { + const versions = ['-', '.', '..', '+', '+test', ' ', '2 .3. 0', '2.', '.2.2', '3.7.2.2', '3.x', ',', '+build-prerelease', '2..2'] + const targetVersion = '2.1.0'; + for (const userVersion of versions) { + const result = semanticVersion.compareVersion(targetVersion, userVersion); + assert.isNull(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); + } + }); + + }); + }); +}); diff --git a/lib/utils/semantic_version/index.ts b/lib/utils/semantic_version/index.ts new file mode 100644 index 000000000..56fad06a5 --- /dev/null +++ b/lib/utils/semantic_version/index.ts @@ -0,0 +1,182 @@ +/** + * Copyright 2020, 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UNKNOWN_MATCH_TYPE } from 'error_message'; +import { LoggerFacade } from '../../logging/logger'; +import { VERSION_TYPE } from '../enums'; + +/** + * Evaluate if provided string is number only + * @param {unknown} content + * @return {boolean} true if the string is number only + * + */ +function isNumber(content: string): boolean { + return /^\d+$/.test(content); +} + +/** + * Evaluate if provided version contains pre-release "-" + * @param {unknown} version + * @return {boolean} true if the version contains "-" and meets condition + * + */ +function isPreReleaseVersion(version: string): boolean { + const preReleaseIndex = version.indexOf(VERSION_TYPE.PRE_RELEASE_VERSION_DELIMITER); + const buildIndex = version.indexOf(VERSION_TYPE.BUILD_VERSION_DELIMITER); + + if (preReleaseIndex < 0) { + return false; + } + + if (buildIndex < 0) { + return true; + } + + return preReleaseIndex < buildIndex; +} + +/** + * Evaluate if provided version contains build "+" + * @param {unknown} version + * @return {boolean} true if the version contains "+" and meets condition + * + */ +function isBuildVersion(version: string): boolean { + const preReleaseIndex = version.indexOf(VERSION_TYPE.PRE_RELEASE_VERSION_DELIMITER); + const buildIndex = version.indexOf(VERSION_TYPE.BUILD_VERSION_DELIMITER); + + if (buildIndex < 0) { + return false; + } + + if (preReleaseIndex < 0) { + return true; + } + + return buildIndex < preReleaseIndex; +} + +/** + * check if there is any white spaces " " in version + * @param {unknown} version + * @return {boolean} true if the version contains " " + * + */ +function hasWhiteSpaces(version: string): boolean { + return /\s/.test(version); +} + +/** + * split version in parts + * @param {unknown} version + * @return {boolean} The array of version split into smaller parts i.e major, minor, patch etc + * null if given version is in invalid format + */ +function splitVersion(version: string, logger?: LoggerFacade): string[] | null { + let targetPrefix = version; + let targetSuffix = ''; + + // check that version shouldn't have white space + if (hasWhiteSpaces(version)) { + logger?.warn(UNKNOWN_MATCH_TYPE, version); + return null; + } + //check for pre release e.g. 1.0.0-alpha where 'alpha' is a pre release + //otherwise check for build e.g. 1.0.0+001 where 001 is a build metadata + if (isPreReleaseVersion(version)) { + targetPrefix = version.substring(0, version.indexOf(VERSION_TYPE.PRE_RELEASE_VERSION_DELIMITER)); + targetSuffix = version.substring(version.indexOf(VERSION_TYPE.PRE_RELEASE_VERSION_DELIMITER) + 1); + } else if (isBuildVersion(version)) { + targetPrefix = version.substring(0, version.indexOf(VERSION_TYPE.BUILD_VERSION_DELIMITER)); + targetSuffix = version.substring(version.indexOf(VERSION_TYPE.BUILD_VERSION_DELIMITER) + 1); + } + + // check dot counts in target_prefix + if (typeof targetPrefix !== 'string' || typeof targetSuffix !== 'string') { + return null; + } + + const dotCount = targetPrefix.split('.').length - 1; + if (dotCount > 2) { + logger?.warn(UNKNOWN_MATCH_TYPE, version); + return null; + } + + const targetVersionParts = targetPrefix.split('.'); + if (targetVersionParts.length != dotCount + 1) { + logger?.warn(UNKNOWN_MATCH_TYPE, version); + return null; + } + for (const part of targetVersionParts) { + if (!isNumber(part)) { + logger?.warn(UNKNOWN_MATCH_TYPE, version); + return null; + } + } + + if (targetSuffix) { + targetVersionParts.push(targetSuffix); + } + + return targetVersionParts; +} + +/** + * Compare user version with condition version + * @param {string} conditionsVersion + * @param {string} userProvidedVersion + * @return {number | null} 0 if user version is equal to condition version + * 1 if user version is greater than condition version + * -1 if user version is less than condition version + * null if invalid user or condition version is provided + */ +export function compareVersion(conditionsVersion: string, userProvidedVersion: string, logger?: LoggerFacade): number | null { + const userVersionParts = splitVersion(userProvidedVersion, logger); + const conditionsVersionParts = splitVersion(conditionsVersion, logger); + + if (!userVersionParts || !conditionsVersionParts) { + return null; + } + + const userVersionPartsLen = userVersionParts.length; + + for (let idx = 0; idx < conditionsVersionParts.length; idx++) { + if (userVersionPartsLen <= idx) { + return isPreReleaseVersion(conditionsVersion) || isBuildVersion(conditionsVersion) ? 1 : -1; + } else if (!isNumber(userVersionParts[idx])) { + if (userVersionParts[idx] < conditionsVersionParts[idx]) { + return isPreReleaseVersion(conditionsVersion) && !isPreReleaseVersion(userProvidedVersion) ? 1 : -1; + } else if (userVersionParts[idx] > conditionsVersionParts[idx]) { + return !isPreReleaseVersion(conditionsVersion) && isPreReleaseVersion(userProvidedVersion) ? -1 : 1; + } + } else { + const userVersionPart = parseInt(userVersionParts[idx]); + const conditionsVersionPart = parseInt(conditionsVersionParts[idx]); + if (userVersionPart > conditionsVersionPart) { + return 1; + } else if (userVersionPart < conditionsVersionPart) { + return -1; + } + } + } + + // check if user version contains release and target version does not + if (isPreReleaseVersion(userProvidedVersion) && !isPreReleaseVersion(conditionsVersion)) { + return -1; + } + + return 0; +} diff --git a/lib/utils/string_value_validator/index.spec.ts b/lib/utils/string_value_validator/index.spec.ts new file mode 100644 index 000000000..a9c7f6a91 --- /dev/null +++ b/lib/utils/string_value_validator/index.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from 'vitest'; +import { validate } from './'; + +describe('validate', () => { + it('should validate the given value is valid string', () => { + expect(validate('validStringValue')).toBe(true); + }); + + it('should return false if given value is invalid string', () => { + expect(validate(null)).toBe(false); + expect(validate(undefined)).toBe(false); + expect(validate('')).toBe(false); + expect(validate(5)).toBe(false); + expect(validate(true)).toBe(false); + expect(validate([])).toBe(false); + }); +}); diff --git a/packages/optimizely-sdk/lib/utils/string_value_validator/index.tests.js b/lib/utils/string_value_validator/index.tests.js similarity index 60% rename from packages/optimizely-sdk/lib/utils/string_value_validator/index.tests.js rename to lib/utils/string_value_validator/index.tests.js index 0937d0dc3..51b9413bd 100644 --- a/packages/optimizely-sdk/lib/utils/string_value_validator/index.tests.js +++ b/lib/utils/string_value_validator/index.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2018, Optimizely + * Copyright 2018, 2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,24 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var chai = require('chai'); -var assert = chai.assert; -var stringInputValidator = require('./'); +import { assert } from 'chai'; +import { validate } from './'; describe('lib/utils/string_input_validator', function() { describe('APIs', function() { describe('validate', function() { it('should validate the given value is valid string', function() { - assert.isTrue(stringInputValidator.validate('validStringValue')); + assert.isTrue(validate('validStringValue')); }); it('should return false if given value is invalid string', function() { - assert.isFalse(stringInputValidator.validate(null)); - assert.isFalse(stringInputValidator.validate(undefined)); - assert.isFalse(stringInputValidator.validate('')); - assert.isFalse(stringInputValidator.validate(5)); - assert.isFalse(stringInputValidator.validate(true)); - assert.isFalse(stringInputValidator.validate([])); + assert.isFalse(validate(null)); + assert.isFalse(validate(undefined)); + assert.isFalse(validate('')); + assert.isFalse(validate(5)); + assert.isFalse(validate(true)); + assert.isFalse(validate([])); }); }); }); diff --git a/lib/utils/string_value_validator/index.ts b/lib/utils/string_value_validator/index.ts new file mode 100644 index 000000000..fd0ceb5f0 --- /dev/null +++ b/lib/utils/string_value_validator/index.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2018, 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Validates provided value is a non-empty string + * @param {unknown} input + * @return {boolean} true for non-empty string, false otherwise + */ +export function validate(input: unknown): boolean { + return typeof input === 'string' && input !== ''; +} diff --git a/lib/utils/type.ts b/lib/utils/type.ts new file mode 100644 index 000000000..c60f85d60 --- /dev/null +++ b/lib/utils/type.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Fn = () => unknown; +export type AsyncFn = () => Promise<unknown>; +export type AsyncTransformer<A, B> = (arg: A) => Promise<B>; +export type Transformer<A, B> = (arg: A) => B; + +export type Consumer<T> = (arg: T) => void; +export type AsyncComsumer<T> = (arg: T) => Promise<void>; + +export type Producer<T> = () => T; +export type AsyncProducer<T> = () => Promise<T>; + +export type Maybe<T> = T | undefined; + +export type Either<A, B> = { type: 'left', value: A } | { type: 'right', value: B }; + +export type OpType = 'sync' | 'async'; +export type OpValue<O extends OpType, V> = O extends 'sync' ? V : Promise<V>; + +export type OrNull<T> = T | null; + +export type Nullable<T, K extends keyof T> = { + [P in keyof T]: P extends K ? OrNull<T[P]> : T[P]; +} diff --git a/lib/utils/user_profile_service_validator/index.spec.ts b/lib/utils/user_profile_service_validator/index.spec.ts new file mode 100644 index 000000000..98a47ef60 --- /dev/null +++ b/lib/utils/user_profile_service_validator/index.spec.ts @@ -0,0 +1,96 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { describe, it, expect } from 'vitest'; +import { validate } from './'; +import { INVALID_USER_PROFILE_SERVICE } from 'error_message'; +import { OptimizelyError } from '../../error/optimizly_error'; + +describe('validate', () => { + it("should throw if the instance does not provide a 'lookup' function", () => { + const missingLookupFunction = { + save: function() {}, + }; + + expect(() => validate(missingLookupFunction)).toThrowError(OptimizelyError); + + try { + validate(missingLookupFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'lookup'"]); + } + }); + + it("should throw if 'lookup' is not a function", () => { + const lookupNotFunction = { + save: function() {}, + lookup: 'notGonnaWork', + }; + + expect(() => validate(lookupNotFunction)).toThrowError(OptimizelyError); + + try { + validate(lookupNotFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'lookup'"]); + } + }); + + it("should throw if the instance does not provide a 'save' function", () => { + const missingSaveFunction = { + lookup: function() {}, + }; + + expect(() => validate(missingSaveFunction)).toThrowError(OptimizelyError); + + try { + validate(missingSaveFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'save'"]); + } + }); + + it("should throw if 'save' is not a function", () => { + const saveNotFunction = { + lookup: function() {}, + save: 'notGonnaWork', + }; + + expect(() => validate(saveNotFunction)).toThrowError(OptimizelyError); + + try { + validate(saveNotFunction); + } catch (err) { + expect(err).instanceOf(OptimizelyError); + expect(err.baseMessage).toBe(INVALID_USER_PROFILE_SERVICE); + expect(err.params).toEqual(["Missing function 'save'"]); + } + }); + + it('should return true if the instance is valid', () => { + const validInstance = { + save: function() {}, + lookup: function() {}, + }; + + expect(validate(validInstance)).toBe(true); + }); +}); diff --git a/packages/optimizely-sdk/lib/utils/user_profile_service_validator/index.tests.js b/lib/utils/user_profile_service_validator/index.tests.js similarity index 50% rename from packages/optimizely-sdk/lib/utils/user_profile_service_validator/index.tests.js rename to lib/utils/user_profile_service_validator/index.tests.js index 88660875f..d5ad136e8 100644 --- a/packages/optimizely-sdk/lib/utils/user_profile_service_validator/index.tests.js +++ b/lib/utils/user_profile_service_validator/index.tests.js @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2017, Optimizely, Inc. and contributors * + * Copyright 2017, 2020, 2022, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -13,53 +13,63 @@ * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ +import { assert } from 'chai'; -var chai = require('chai'); -var assert = chai.assert; -var sprintf = require('sprintf-js').sprintf; -var userProfileServiceValidator = require('./'); - -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; +import { validate } from './'; +import { INVALID_USER_PROFILE_SERVICE } from 'error_message'; describe('lib/utils/user_profile_service_validator', function() { describe('APIs', function() { describe('validate', function() { - it('should throw if the instance does not provide a \'lookup\' function', function() { + it("should throw if the instance does not provide a 'lookup' function", function() { var missingLookupFunction = { - save: function() {} + save: function() {}, }; - assert.throws(function() { - userProfileServiceValidator.validate(missingLookupFunction); - }, sprintf(ERROR_MESSAGES.INVALID_USER_PROFILE_SERVICE, 'USER_PROFILE_SERVICE_VALIDATOR', 'Missing function \'lookup\'')); + const ex = assert.throws(function() { + validate(missingLookupFunction); + }); + assert.equal(ex.baseMessage, INVALID_USER_PROFILE_SERVICE); + assert.deepEqual(ex.params, ["Missing function 'lookup'"]); }); - it('should throw if \'lookup\' is not a function', function() { + it("should throw if 'lookup' is not a function", function() { var lookupNotFunction = { save: function() {}, lookup: 'notGonnaWork', }; - assert.throws(function() { - userProfileServiceValidator.validate(lookupNotFunction); - }, sprintf(ERROR_MESSAGES.INVALID_USER_PROFILE_SERVICE, 'USER_PROFILE_SERVICE_VALIDATOR', 'Missing function \'lookup\'')); + const ex = assert.throws(function() { + validate(lookupNotFunction); + }); + assert.equal(ex.baseMessage, INVALID_USER_PROFILE_SERVICE); + assert.deepEqual(ex.params, ["Missing function 'lookup'"]); }); - it('should throw if the instance does not provide a \'save\' function', function() { + it("should throw if the instance does not provide a 'save' function", function() { var missingSaveFunction = { - lookup: function() {} + lookup: function() {}, }; - assert.throws(function() { - userProfileServiceValidator.validate(missingSaveFunction); - }, sprintf(ERROR_MESSAGES.INVALID_USER_PROFILE_SERVICE, 'USER_PROFILE_SERVICE_VALIDATOR', 'Missing function \'save\'')); + const ex = assert.throws(function() { + validate(missingSaveFunction); + }); + assert.equal(ex.baseMessage, INVALID_USER_PROFILE_SERVICE); + assert.deepEqual(ex.params, ["Missing function 'save'"]); + // , sprintf( + // INVALID_USER_PROFILE_SERVICE, + // 'USER_PROFILE_SERVICE_VALIDATOR', + // "Missing function 'save'" + // )); }); - it('should throw if \'save\' is not a function', function() { + it("should throw if 'save' is not a function", function() { var saveNotFunction = { lookup: function() {}, save: 'notGonnaWork', }; - assert.throws(function() { - userProfileServiceValidator.validate(saveNotFunction); - }, sprintf(ERROR_MESSAGES.INVALID_USER_PROFILE_SERVICE, 'USER_PROFILE_SERVICE_VALIDATOR', 'Missing function \'save\'')); + const ex = assert.throws(function() { + validate(saveNotFunction); + }); + assert.equal(ex.baseMessage, INVALID_USER_PROFILE_SERVICE); + assert.deepEqual(ex.params, ["Missing function 'save'"]); }); it('should return true if the instance is valid', function() { @@ -67,7 +77,7 @@ describe('lib/utils/user_profile_service_validator', function() { save: function() {}, lookup: function() {}, }; - assert.isTrue(userProfileServiceValidator.validate(validInstance)); + assert.isTrue(validate(validInstance)); }); }); }); diff --git a/packages/optimizely-sdk/lib/utils/user_profile_service_validator/index.js b/lib/utils/user_profile_service_validator/index.ts similarity index 52% rename from packages/optimizely-sdk/lib/utils/user_profile_service_validator/index.js rename to lib/utils/user_profile_service_validator/index.ts index 366e1e20f..95e8cf61a 100644 --- a/packages/optimizely-sdk/lib/utils/user_profile_service_validator/index.js +++ b/lib/utils/user_profile_service_validator/index.ts @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2017, Optimizely, Inc. and contributors * + * Copyright 2017, 2020, 2022, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -18,24 +18,26 @@ * Provides utility method for validating that the given user profile service implementation is valid. */ -var sprintf = require('sprintf-js').sprintf; +import { ObjectWithUnknownProperties } from '../../shared_types'; +import { INVALID_USER_PROFILE_SERVICE } from 'error_message'; -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; -var MODULE_NAME = 'USER_PROFILE_SERVICE_VALIDATOR'; +import { OptimizelyError } from '../../error/optimizly_error'; -module.exports = { - /** - * Validates user's provided user profile service instance - * @param {Object} userProfileServiceInstance - * @return {boolean} True if the instance is valid - * @throws If the instance is not valid - */ - validate: function(userProfileServiceInstance) { - if (typeof userProfileServiceInstance.lookup !== 'function') { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_USER_PROFILE_SERVICE, MODULE_NAME, 'Missing function \'lookup\'')); - } else if (typeof userProfileServiceInstance.save !== 'function') { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_USER_PROFILE_SERVICE, MODULE_NAME, 'Missing function \'save\'')); +/** + * Validates user's provided user profile service instance + * @param {unknown} userProfileServiceInstance + * @return {boolean} true if the instance is valid + * @throws If the instance is not valid + */ + +export function validate(userProfileServiceInstance: unknown): boolean { + if (typeof userProfileServiceInstance === 'object' && userProfileServiceInstance !== null) { + if (typeof (userProfileServiceInstance as ObjectWithUnknownProperties)['lookup'] !== 'function') { + throw new OptimizelyError(INVALID_USER_PROFILE_SERVICE, "Missing function 'lookup'"); + } else if (typeof (userProfileServiceInstance as ObjectWithUnknownProperties)['save'] !== 'function') { + throw new OptimizelyError(INVALID_USER_PROFILE_SERVICE, "Missing function 'save'"); } return true; - }, -}; + } + throw new OptimizelyError(INVALID_USER_PROFILE_SERVICE, 'Not an object'); +} diff --git a/lib/vuid/vuid.spec.ts b/lib/vuid/vuid.spec.ts new file mode 100644 index 000000000..0a0790b59 --- /dev/null +++ b/lib/vuid/vuid.spec.ts @@ -0,0 +1,39 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from 'vitest'; + +import { isVuid, makeVuid, VUID_MAX_LENGTH } from './vuid'; + +describe('isVuid', () => { + it('should return true if and only if the value strats with the VUID_PREFIX and is longer than vuid_prefix', () => { + expect(isVuid('vuid_a')).toBe(true); + expect(isVuid('vuid_123')).toBe(true); + expect(isVuid('vuid_')).toBe(false); + expect(isVuid('vuid')).toBe(false); + expect(isVuid('vui')).toBe(false); + expect(isVuid('vu_123')).toBe(false); + expect(isVuid('123')).toBe(false); + }) +}); + +describe('makeVuid', () => { + it('should return a string that is a valid vuid and whose length is within VUID_MAX_LENGTH', () => { + const vuid = makeVuid(); + expect(isVuid(vuid)).toBe(true); + expect(vuid.length).toBeLessThanOrEqual(VUID_MAX_LENGTH); + }); +}); diff --git a/lib/vuid/vuid.ts b/lib/vuid/vuid.ts new file mode 100644 index 000000000..d335c329d --- /dev/null +++ b/lib/vuid/vuid.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { v4 as uuidV4 } from 'uuid'; + +export const VUID_PREFIX = `vuid_`; +export const VUID_MAX_LENGTH = 32; + +export const isVuid = (vuid: string): boolean => vuid.startsWith(VUID_PREFIX) && vuid.length > VUID_PREFIX.length; + +export const makeVuid = (): string => { + // make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details. + const uuid = uuidV4(); + const formatted = uuid.replace(/-/g, ''); + const vuidFull = `${VUID_PREFIX}${formatted}`; + + return vuidFull.length <= VUID_MAX_LENGTH ? vuidFull : vuidFull.substring(0, VUID_MAX_LENGTH); +}; diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts new file mode 100644 index 000000000..3cfb2a608 --- /dev/null +++ b/lib/vuid/vuid_manager.spec.ts @@ -0,0 +1,230 @@ +/** + * Copyright 2022, 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect, vi } from 'vitest'; + +import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; + +import { getMockAsyncCache } from '../tests/mock/mock_cache'; +import { isVuid } from './vuid'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { exhaustMicrotasks } from '../tests/testUtils'; + + +const vuidCacheKey = 'optimizely-vuid'; + +describe('VuidCacheManager', () => { + it('should remove vuid from cache', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set(vuidCacheKey, 'vuid_valid'); + + const manager = new VuidCacheManager(cache); + await manager.remove(); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBeUndefined(); + }); + + it('should create and save a new vuid if there is no vuid in cache', async () => { + const cache = getMockAsyncCache<string>(); + + const manager = new VuidCacheManager(cache); + const vuid = await manager.load(); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe(vuid); + expect(isVuid(vuid!)).toBe(true); + }); + + it('should create and save a new vuid if old VUID from cache is not valid', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set(vuidCacheKey, 'invalid-vuid'); + + const manager = new VuidCacheManager(cache); + const vuid = await manager.load(); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe(vuid); + expect(isVuid(vuid!)).toBe(true); + }); + + it('should return the same vuid without modifying the cache after creating a new vuid', async () => { + const cache = getMockAsyncCache<string>(); + + const manager = new VuidCacheManager(cache); + const vuid1 = await manager.load(); + const vuid2 = await manager.load(); + expect(vuid1).toBe(vuid2); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe(vuid1); + }); + + it('should use the vuid in cache if available', async () => { + const cache = getMockAsyncCache<string>(); + await cache.set(vuidCacheKey, 'vuid_valid'); + + const manager = new VuidCacheManager(cache); + const vuid1 = await manager.load(); + const vuid2 = await manager.load(); + expect(vuid1).toBe('vuid_valid'); + expect(vuid2).toBe('vuid_valid'); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe('vuid_valid'); + }); + + it('should use the new cache after setCache is called', async () => { + const cache1 = getMockAsyncCache<string>(); + const cache2 = getMockAsyncCache<string>(); + + await cache1.set(vuidCacheKey, 'vuid_123'); + await cache2.set(vuidCacheKey, 'vuid_456'); + + const manager = new VuidCacheManager(cache1); + const vuid1 = await manager.load(); + expect(vuid1).toBe('vuid_123'); + + manager.setCache(cache2); + await manager.load(); + const vuid2 = await cache2.get(vuidCacheKey); + expect(vuid2).toBe('vuid_456'); + + await manager.remove(); + const vuidInCache = await cache2.get(vuidCacheKey); + expect(vuidInCache).toBeUndefined(); + }); + + it('should sequence remove and load calls', async() => { + const cache = getMockAsyncCache<string>(); + const removeSpy = vi.spyOn(cache, 'remove'); + const getSpy = vi.spyOn(cache, 'get'); + const setSpy = vi.spyOn(cache, 'set'); + + const removePromise = resolvablePromise(); + removeSpy.mockReturnValueOnce(removePromise.promise); + + const getPromise = resolvablePromise<string>(); + getSpy.mockReturnValueOnce(getPromise.promise); + + const setPromise = resolvablePromise(); + setSpy.mockReturnValueOnce(setPromise.promise); + + const manager = new VuidCacheManager(cache); + + // this should try to remove from cache, which should stay pending + const call1 = manager.remove(); + + // this should try to get the vuid from cache + const call2 = manager.load(); + + // this should again try to remove vuid + const call3 = manager.remove(); + + await exhaustMicrotasks(); + + expect(removeSpy).toHaveBeenCalledTimes(1); // from the first manager.remove call + expect(getSpy).not.toHaveBeenCalled(); + + // this will resolve the first manager.remove call + removePromise.resolve(true); + await exhaustMicrotasks(); + await expect(call1).resolves.not.toThrow(); + + // this get call is from the load call + expect(getSpy).toHaveBeenCalledTimes(1); + await exhaustMicrotasks(); + + // as the get call is pending, remove call from the second manager.remove call should not yet happen + expect(removeSpy).toHaveBeenCalledTimes(1); + + // this should fail the load call, allowing the second remnove call to proceed + getPromise.reject(new Error('get failed')); + await exhaustMicrotasks(); + await expect(call2).rejects.toThrow(); + + expect(removeSpy).toHaveBeenCalledTimes(2); + }); +}); + +describe('DefaultVuidManager', () => { + const getMockCacheManager = () => ({ + remove: vi.fn(), + load: vi.fn(), + setCache: vi.fn(), + }); + + it('should return undefined for getVuid() before initialization', async () => { + const manager = new DefaultVuidManager({ + vuidCache: getMockAsyncCache<string>(), + vuidCacheManager: getMockCacheManager() as unknown as VuidCacheManager, + enableVuid: true + }); + + expect(manager.getVuid()).toBeUndefined(); + }); + + it('should set the cache on vuidCacheManager', async () => { + const vuidCacheManager = getMockCacheManager(); + + const cache = getMockAsyncCache<string>(); + + const manager = new DefaultVuidManager({ + vuidCache: cache, + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, + enableVuid: false + }); + + await manager.initialize(); + expect(vuidCacheManager.setCache).toHaveBeenCalledWith(cache); + }); + + it('should call remove on VuidCacheManager if enableVuid is false', async () => { + const vuidCacheManager = getMockCacheManager(); + + const manager = new DefaultVuidManager({ + vuidCache: getMockAsyncCache<string>(), + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, + enableVuid: false + }); + + await manager.initialize(); + expect(vuidCacheManager.remove).toHaveBeenCalled(); + }); + + it('should return undefined for getVuid() after initialization if enableVuid is false', async () => { + const vuidCacheManager = getMockCacheManager(); + + const manager = new DefaultVuidManager({ + vuidCache: getMockAsyncCache<string>(), + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, + enableVuid: false + }); + + await manager.initialize(); + expect(manager.getVuid()).toBeUndefined(); + }); + + it('should load vuid using VuidCacheManger if enableVuid=true', async () => { + const vuidCacheManager = getMockCacheManager(); + vuidCacheManager.load.mockResolvedValue('vuid_valid'); + + const manager = new DefaultVuidManager({ + vuidCache: getMockAsyncCache<string>(), + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, + enableVuid: true + }); + + await manager.initialize(); + expect(vuidCacheManager.load).toHaveBeenCalled(); + expect(manager.getVuid()).toBe('vuid_valid'); + }); +}); diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts new file mode 100644 index 000000000..dd0c0322a --- /dev/null +++ b/lib/vuid/vuid_manager.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2022-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { LoggerFacade } from '../logging/logger'; +import { Store } from '../utils/cache/store'; +import { AsyncProducer, Maybe } from '../utils/type'; +import { isVuid, makeVuid } from './vuid'; + +export interface VuidManager { + getVuid(): Maybe<string>; + isVuidEnabled(): boolean; + initialize(): Promise<void>; +} + +export class VuidCacheManager { + private logger?: LoggerFacade; + private vuidCacheKey = 'optimizely-vuid'; + private cache?: Store<string>; + // if this value is not undefined, this means the same value is in the cache. + // if this is undefined, it could either mean that there is no value in the cache + // or that there is a value in the cache but it has not been loaded yet or failed + // to load. + private vuid?: string; + private waitPromise: Promise<unknown> = Promise.resolve(); + + constructor(cache?: Store<string>, logger?: LoggerFacade) { + this.cache = cache; + this.logger = logger; + } + + setCache(cache: Store<string>): void { + this.cache = cache; + this.vuid = undefined; + } + + setLogger(logger: LoggerFacade): void { + this.logger = logger; + } + + private async serialize<T>(fn: AsyncProducer<T>): Promise<T> { + const resultPromise = this.waitPromise.then(fn, fn); + this.waitPromise = resultPromise.catch(() => {}); + return resultPromise; + } + + async remove(): Promise<unknown> { + const removeFn = async () => { + if (!this.cache) { + return; + } + this.vuid = undefined; + await this.cache.remove(this.vuidCacheKey); + } + + return this.serialize(removeFn); + } + + async load(): Promise<Maybe<string>> { + if (this.vuid) { + return this.vuid; + } + + const loadFn = async () => { + if (!this.cache) { + return; + } + const cachedValue = await this.cache.get(this.vuidCacheKey); + if (cachedValue && isVuid(cachedValue)) { + this.vuid = cachedValue; + return this.vuid; + } + const newVuid = makeVuid(); + await this.cache.set(this.vuidCacheKey, newVuid); + this.vuid = newVuid; + return newVuid; + } + return this.serialize(loadFn); + } +} + +export type VuidManagerConfig = { + enableVuid?: boolean; + vuidCache: Store<string>; + vuidCacheManager: VuidCacheManager; +} + +export class DefaultVuidManager implements VuidManager { + private vuidCacheManager: VuidCacheManager; + private vuid?: string; + private vuidCache: Store<string>; + private vuidEnabled = false; + + constructor(config: VuidManagerConfig) { + this.vuidCacheManager = config.vuidCacheManager; + this.vuidEnabled = config.enableVuid || false; + this.vuidCache = config.vuidCache; + } + + getVuid(): Maybe<string> { + return this.vuid; + } + + isVuidEnabled(): boolean { + return this.vuidEnabled; + } + + /** + * initializes the VuidManager + * @returns Promise that resolves when the VuidManager is initialized + */ + async initialize(): Promise<void> { + this.vuidCacheManager.setCache(this.vuidCache); + if (!this.vuidEnabled) { + await this.vuidCacheManager.remove(); + return; + } + + this.vuid = await this.vuidCacheManager.load(); + } +} diff --git a/lib/vuid/vuid_manager_factory.browser.spec.ts b/lib/vuid/vuid_manager_factory.browser.spec.ts new file mode 100644 index 000000000..59c8602db --- /dev/null +++ b/lib/vuid/vuid_manager_factory.browser.spec.ts @@ -0,0 +1,85 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, describe, expect, it, beforeEach } from 'vitest'; + +vi.mock('../utils/cache/local_storage_cache.browser', () => { + return { + LocalStorageCache: vi.fn(), + }; +}); + +vi.mock('./vuid_manager', () => { + return { + DefaultVuidManager: vi.fn(), + VuidCacheManager: vi.fn(), + }; +}); + +import { getMockSyncCache } from '../tests/mock/mock_cache'; +import { createVuidManager } from './vuid_manager_factory.browser'; +import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; +import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; +import { extractVuidManager } from './vuid_manager_factory'; + +describe('createVuidManager', () => { + const MockVuidCacheManager = vi.mocked(VuidCacheManager); + const MockLocalStorageCache = vi.mocked(LocalStorageCache); + const MockDefaultVuidManager = vi.mocked(DefaultVuidManager); + + beforeEach(() => { + MockLocalStorageCache.mockClear(); + MockDefaultVuidManager.mockClear(); + }); + + it('should pass the enableVuid option to the DefaultVuidManager', () => { + const manager = extractVuidManager(createVuidManager({ enableVuid: true })); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].enableVuid).toBe(true); + + const manager2 = extractVuidManager(createVuidManager({ enableVuid: false })); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockDefaultVuidManager.mock.calls[1][0].enableVuid).toBe(false); + }); + + it('should use the provided cache', () => { + const cache = getMockSyncCache<string>(); + const manager = extractVuidManager(createVuidManager({ enableVuid: true, vuidCache: cache })); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].vuidCache).toBe(cache); + }); + + it('should use a LocalStorageCache if no cache is provided', () => { + const manager = extractVuidManager(createVuidManager({ enableVuid: true })); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + + const usedCache = MockDefaultVuidManager.mock.calls[0][0].vuidCache; + expect(usedCache).toBe(MockLocalStorageCache.mock.instances[0]); + }); + + it('should use a single VuidCacheManager instance for all VuidManager instances', () => { + const manager1 = extractVuidManager(createVuidManager({ enableVuid: true })); + const manager2 = extractVuidManager(createVuidManager({ enableVuid: true })); + expect(manager1).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockVuidCacheManager.mock.instances.length).toBe(1); + + const usedCacheManager1 = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; + const usedCacheManager2 = MockDefaultVuidManager.mock.calls[1][0].vuidCacheManager; + expect(usedCacheManager1).toBe(usedCacheManager2); + expect(usedCacheManager1).toBe(MockVuidCacheManager.mock.instances[0]); + }); +}); diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts new file mode 100644 index 000000000..0691fd5e7 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -0,0 +1,28 @@ +/** +* Copyright 2024-2025, Optimizely +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; +import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; +import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; + +export const vuidCacheManager = new VuidCacheManager(); + +export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { + return wrapVuidManager(new DefaultVuidManager({ + vuidCacheManager, + vuidCache: options.vuidCache || new LocalStorageCache<string>(), + enableVuid: options.enableVuid + })); +}; diff --git a/lib/vuid/vuid_manager_factory.node.spec.ts b/lib/vuid/vuid_manager_factory.node.spec.ts new file mode 100644 index 000000000..8f6b21e74 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.node.spec.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, describe, expect, it } from 'vitest'; + +import { createVuidManager } from './vuid_manager_factory.node'; +import { extractVuidManager } from './vuid_manager_factory'; + +describe('createVuidManager', () => { + it('should return a undefined vuid manager wrapped as OpaqueVuidManager', () => { + expect(extractVuidManager(createVuidManager({ enableVuid: true }))) + .toBeUndefined(); + }); +}); diff --git a/lib/vuid/vuid_manager_factory.node.ts b/lib/vuid/vuid_manager_factory.node.ts new file mode 100644 index 000000000..439e70ec1 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.node.ts @@ -0,0 +1,20 @@ +/** +* Copyright 2024-2025, Optimizely +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; + +export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { + return wrapVuidManager(undefined); +}; diff --git a/lib/vuid/vuid_manager_factory.react_native.spec.ts b/lib/vuid/vuid_manager_factory.react_native.spec.ts new file mode 100644 index 000000000..8057946e3 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.react_native.spec.ts @@ -0,0 +1,86 @@ +/** + * Copyright 2024, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi, describe, expect, it, beforeEach } from 'vitest'; + +vi.mock('../utils/cache/async_storage_cache.react_native', () => { + return { + AsyncStorageCache: vi.fn(), + }; +}); + +vi.mock('./vuid_manager', () => { + return { + DefaultVuidManager: vi.fn(), + VuidCacheManager: vi.fn(), + }; +}); + +import { getMockAsyncCache } from '../tests/mock/mock_cache'; +import { createVuidManager } from './vuid_manager_factory.react_native'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; + +import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; +import { extractVuidManager } from './vuid_manager_factory'; + +describe('extractVuidManager(createVuidManager', () => { + const MockVuidCacheManager = vi.mocked(VuidCacheManager); + const MockAsyncStorageCache = vi.mocked(AsyncStorageCache); + const MockDefaultVuidManager = vi.mocked(DefaultVuidManager); + + beforeEach(() => { + MockAsyncStorageCache.mockClear(); + MockDefaultVuidManager.mockClear(); + }); + + it('should pass the enableVuid option to the DefaultVuidManager', () => { + const manager = extractVuidManager(createVuidManager({ enableVuid: true })); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].enableVuid).toBe(true); + + const manager2 = extractVuidManager(createVuidManager({ enableVuid: false })); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockDefaultVuidManager.mock.calls[1][0].enableVuid).toBe(false); + }); + + it('should use the provided cache', () => { + const cache = getMockAsyncCache<string>(); + const manager = extractVuidManager(createVuidManager({ enableVuid: true, vuidCache: cache })); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].vuidCache).toBe(cache); + }); + + it('should use a AsyncStorageCache if no cache is provided', () => { + const manager = extractVuidManager(createVuidManager({ enableVuid: true })); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + + const usedCache = MockDefaultVuidManager.mock.calls[0][0].vuidCache; + expect(usedCache).toBe(MockAsyncStorageCache.mock.instances[0]); + }); + + it('should use a single VuidCacheManager instance for all VuidManager instances', () => { + const manager1 = extractVuidManager(createVuidManager({ enableVuid: true })); + const manager2 = extractVuidManager(createVuidManager({ enableVuid: true })); + expect(manager1).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockVuidCacheManager.mock.instances.length).toBe(1); + + const usedCacheManager1 = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; + const usedCacheManager2 = MockDefaultVuidManager.mock.calls[1][0].vuidCacheManager; + expect(usedCacheManager1).toBe(usedCacheManager2); + expect(usedCacheManager1).toBe(MockVuidCacheManager.mock.instances[0]); + }); +}); diff --git a/lib/vuid/vuid_manager_factory.react_native.ts b/lib/vuid/vuid_manager_factory.react_native.ts new file mode 100644 index 000000000..0aeb1c537 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.react_native.ts @@ -0,0 +1,28 @@ +/** +* Copyright 2024-2025, Optimizely +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; +import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; + +export const vuidCacheManager = new VuidCacheManager(); + +export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { + return wrapVuidManager(new DefaultVuidManager({ + vuidCacheManager, + vuidCache: options.vuidCache || new AsyncStorageCache<string>(), + enableVuid: options.enableVuid + })); +}; diff --git a/lib/vuid/vuid_manager_factory.ts b/lib/vuid/vuid_manager_factory.ts new file mode 100644 index 000000000..f7f1b760f --- /dev/null +++ b/lib/vuid/vuid_manager_factory.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Store } from '../utils/cache/store'; +import { Maybe } from '../utils/type'; +import { VuidManager } from './vuid_manager'; + +export type VuidManagerOptions = { + vuidCache?: Store<string>; + enableVuid?: boolean; +} + +const vuidManagerSymbol: unique symbol = Symbol(); + +export type OpaqueVuidManager = { + [vuidManagerSymbol]: unknown; +}; + +export const extractVuidManager = (opaqueVuidManager: Maybe<OpaqueVuidManager>): Maybe<VuidManager> => { + if (!opaqueVuidManager || typeof opaqueVuidManager !== 'object') { + return undefined; + } + + return opaqueVuidManager[vuidManagerSymbol] as Maybe<VuidManager>; +}; + +export const wrapVuidManager = (vuidManager: Maybe<VuidManager>): OpaqueVuidManager => { + return { + [vuidManagerSymbol]: vuidManager + } +}; diff --git a/message_generator.ts b/message_generator.ts new file mode 100644 index 000000000..fae725a1c --- /dev/null +++ b/message_generator.ts @@ -0,0 +1,36 @@ +import path from 'path'; +import { writeFile } from 'fs/promises'; + +const generate = async () => { + const inp = process.argv.slice(2); + for(const filePath of inp) { + console.log('generating messages for: ', filePath); + const parsedPath = path.parse(filePath); + const fileName = parsedPath.name; + const dirName = parsedPath.dir; + const ext = parsedPath.ext; + + const genFilePath = path.join(dirName, `${fileName}.gen${ext}`); + console.log('generated file path: ', genFilePath); + const exports = await import(filePath); + const messages : Array<any> = []; + + let genOut = ''; + + Object.keys(exports).forEach((key, i) => { + if (key === 'messages') return; + genOut += `export const ${key} = '${i}';\n`; + messages.push(exports[key]) + }); + + genOut += `export const messages = ${JSON.stringify(messages, null, 2)};` + await writeFile(genFilePath, genOut, 'utf-8'); + } +} + +generate().then(() => { + console.log('successfully generated messages'); +}).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json index e556c97c9..4820c783c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5054 +1,14599 @@ { - "name": "optimizely-sdk-packages", - "version": "1.0.0", - "lockfileVersion": 1, + "name": "@optimizely/optimizely-sdk", + "version": "6.1.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@lerna/add": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.2.0.tgz", - "integrity": "sha512-qGA7agAWcKlrXZR3FwFJXTr26Q2rqjOVMNhtm8uyawImqfdKp4WJXuGdioiWOSW20jMvzLIFhWZh5lCh0UyMBw==", - "requires": { - "@lerna/bootstrap": "^3.2.0", - "@lerna/command": "^3.1.3", - "@lerna/filter-options": "^3.1.2", - "@lerna/npm-conf": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "dedent": "^0.7.0", - "npm-package-arg": "^6.0.0", - "p-map": "^1.2.0", - "pacote": "^9.1.0", - "semver": "^5.5.0" - } - }, - "@lerna/batch-packages": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.1.2.tgz", - "integrity": "sha512-HAkpptrYeUVlBYbLScXgeCgk6BsNVXxDd53HVWgzzTWpXV4MHpbpeKrByyt7viXlNhW0w73jJbipb/QlFsHIhQ==", - "requires": { - "@lerna/package-graph": "^3.1.2", - "@lerna/validation-error": "^3.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/bootstrap": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.2.0.tgz", - "integrity": "sha512-xh6dPpdzsAEWF7lqASaym5AThkmP3ArR7Q+P/tiPWCT+OT7QT5QI2IQAz1aAYEBQL3ACzpE6kq+VOGi0m+9bxw==", - "requires": { - "@lerna/batch-packages": "^3.1.2", - "@lerna/command": "^3.1.3", - "@lerna/filter-options": "^3.1.2", - "@lerna/has-npm-version": "^3.0.4", - "@lerna/npm-conf": "^3.0.0", - "@lerna/npm-install": "^3.0.0", - "@lerna/rimraf-dir": "^3.0.0", - "@lerna/run-lifecycle": "^3.2.0", - "@lerna/run-parallel-batches": "^3.0.0", - "@lerna/symlink-binary": "^3.1.4", - "@lerna/symlink-dependencies": "^3.1.4", - "@lerna/validation-error": "^3.0.0", - "dedent": "^0.7.0", - "get-port": "^3.2.0", - "multimatch": "^2.1.0", - "npm-package-arg": "^6.0.0", - "npmlog": "^4.1.2", - "p-finally": "^1.0.0", - "p-map": "^1.2.0", - "p-map-series": "^1.0.0", - "p-waterfall": "^1.0.0", - "read-package-tree": "^5.1.6", - "semver": "^5.5.0" - } - }, - "@lerna/changed": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.2.0.tgz", - "integrity": "sha512-R+vGzzXPN5s5lJT0v1zSTLw43O2ek2yekqCqvw7p9UFqgqYSbxUsyWXMdhku/mOIFWTc6DzrsOi+U7CX3TXmHg==", - "requires": { - "@lerna/collect-updates": "^3.1.0", - "@lerna/command": "^3.1.3", - "@lerna/listable": "^3.0.0", - "@lerna/output": "^3.0.0", - "@lerna/version": "^3.2.0" - } - }, - "@lerna/check-working-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.1.0.tgz", - "integrity": "sha512-ruy6s44IUcaPEa4JlDDDk6nbacMESUPRSb+dohzLJxfhXx1wFnEVF6L91TGxFP+C0lt5V6zd8rnJxkW/uZzNAA==", - "requires": { - "@lerna/describe-ref": "^3.1.0", - "@lerna/validation-error": "^3.0.0" - } - }, - "@lerna/child-process": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.0.0.tgz", - "integrity": "sha512-8vHRDkpGhzSaMsXgyXVgY80mUSC5WSkDmhWWA3bnB/n5FBK1gK8EKQUpHTk14SckwvUgEJzBd35gR5/XKGOgmQ==", - "requires": { - "chalk": "^2.3.1", - "execa": "^0.10.0", - "strong-log-transformer": "^1.0.6" - } - }, - "@lerna/clean": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.1.3.tgz", - "integrity": "sha512-XVdcIOjhudXlk5pTXjrpsnNLqeVi2rBu2oWzPH2GHrxWGBZBW8thGIFhQf09da/RbRT3uzBWXpUv+sbL2vbX3g==", - "requires": { - "@lerna/command": "^3.1.3", - "@lerna/filter-options": "^3.1.2", - "@lerna/prompt": "^3.0.0", - "@lerna/rimraf-dir": "^3.0.0", - "p-map": "^1.2.0", - "p-map-series": "^1.0.0", - "p-waterfall": "^1.0.0" - } - }, - "@lerna/cli": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.2.0.tgz", - "integrity": "sha512-JdbLyTxHqxUlrkI+Ke+ltXbtyA+MPu9zR6kg/n8Fl6uaez/2fZWtReXzYi8MgLxfUFa7+1OHWJv4eAMZlByJ+Q==", - "requires": { - "@lerna/global-options": "^3.1.3", - "dedent": "^0.7.0", - "npmlog": "^4.1.2", - "yargs": "^12.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", - "requires": { - "xregexp": "4.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } + "packages": { + "": { + "name": "@optimizely/optimizely-sdk", + "version": "6.1.0", + "license": "Apache-2.0", + "dependencies": { + "decompress-response": "^7.0.0", + "json-schema": "^0.4.0", + "murmurhash": "^2.0.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@react-native-async-storage/async-storage": "^2", + "@react-native-community/netinfo": "^11.3.2", + "@rollup/plugin-commonjs": "^11.0.2", + "@rollup/plugin-node-resolve": "^7.1.1", + "@types/chai": "^4.2.11", + "@types/mocha": "^5.2.7", + "@types/nise": "^1.4.0", + "@types/node": "^18.7.18", + "@types/ua-parser-js": "^0.7.36", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^5.33.0", + "@typescript-eslint/parser": "^5.33.0", + "@vitest/coverage-istanbul": "^2.0.5", + "chai": "^4.2.0", + "coveralls-next": "^4.2.0", + "eslint": "^8.21.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", + "happy-dom": "^16.6.0", + "jiti": "^2.4.1", + "karma": "^6.4.0", + "karma-browserstack-launcher": "^1.5.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.1.1", + "karma-mocha": "^2.0.1", + "karma-webpack": "^5.0.1", + "lodash": "^4.17.11", + "mocha": "^10.2.0", + "mocha-lcov-reporter": "^1.3.0", + "nise": "^1.4.10", + "nock": "11.9.1", + "nyc": "^15.0.1", + "prettier": "^1.19.1", + "promise-polyfill": "8.1.0", + "rollup": "2.79.2", + "rollup-plugin-terser": "^5.3.0", + "rollup-plugin-typescript2": "^0.27.1", + "sinon": "^2.3.1", + "ts-loader": "^9.3.1", + "ts-node": "^8.10.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^4.7.4", + "vitest": "^2.0.5", + "webpack": "^5.74.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": ">=1.0.0 <3.0.0", + "@react-native-community/netinfo": ">=5.0.0 <12.0.0", + "fast-text-encoding": "^1.0.6", + "react-native-get-random-values": "^1.11.0", + "ua-parser-js": "^1.0.38" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" + "@react-native-community/netinfo": { + "optional": true }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } + "fast-text-encoding": { + "optional": true }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } + "react-native-get-random-values": { + "optional": true }, - "yargs": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.1.tgz", - "integrity": "sha512-B0vRAp1hRX4jgIOWFtjfNjd9OA9RWYZ6tqGA9/I/IrTMsxmKvtWy+ersM+jzpQqbC3YfLzeABPdeTgcJ9eu1qQ==", - "requires": { - "cliui": "^4.0.0", - "decamelize": "^2.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" - } + "ua-parser-js": { + "optional": true } } }, - "@lerna/collect-updates": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.1.0.tgz", - "integrity": "sha512-zHxHRZOteTqcW9mAyLrmoWEKpfxgA3c6LJj4nauB2pM3MKyKNhg0gqiy2RHFu7EGivPki4Q1624I301iAXtUVA==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/describe-ref": "^3.1.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "slash": "^1.0.0" + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "@lerna/command": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.1.3.tgz", - "integrity": "sha512-ptaFNsfcTpxnSkaNrGgW3fRbWWVSVDUx4BpkjUUnRTgy9mwoykQWgQB3inhgTYHwW6e4PzO79F2hovfUMzHuzg==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/package-graph": "^3.1.2", - "@lerna/project": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "@lerna/write-log-file": "^3.0.0", - "dedent": "^0.7.0", - "execa": "^0.10.0", - "is-ci": "^1.0.10", - "lodash": "^4.17.5", - "npmlog": "^4.1.2" - } - }, - "@lerna/conventional-commits": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.0.2.tgz", - "integrity": "sha512-Cxd1eWXn3usADKXIUvYmTERx2+1N7oJD4Whz+FVu8kTfufsfTO7fYOan1RVkg86ukZbNDyS+iOxZ8DJ2JspS9g==", - "requires": { - "@lerna/validation-error": "^3.0.0", - "conventional-changelog-angular": "^1.6.6", - "conventional-changelog-core": "^2.0.5", - "conventional-recommended-bump": "^2.0.6", - "dedent": "^0.7.0", - "fs-extra": "^6.0.1", - "get-stream": "^3.0.0", - "npm-package-arg": "^6.0.0", - "npmlog": "^4.1.2", - "semver": "^5.5.0" - } - }, - "@lerna/create": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.1.3.tgz", - "integrity": "sha512-CmXKCBc6AE3F9O6mMg4Y76cQ8eTCy3ksqDFKxVQMdYDtHKnTrH1s0l8sn3J1AiylqVDnvxhb3Rjyj+OWyzmFPQ==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/command": "^3.1.3", - "@lerna/npm-conf": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "camelcase": "^4.1.0", - "dedent": "^0.7.0", - "fs-extra": "^6.0.1", - "globby": "^8.0.1", - "init-package-json": "^1.10.3", - "npm-package-arg": "^6.0.0", - "pify": "^3.0.0", - "semver": "^5.5.0", - "slash": "^1.0.0", - "validate-npm-package-license": "^3.0.3", - "validate-npm-package-name": "^3.0.0", - "whatwg-url": "^6.5.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" } }, - "@lerna/create-symlink": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.0.0.tgz", - "integrity": "sha512-Q9qAzGGqQtVzHWrCz+Md4SH0tW99DrgFJ68cnFqilOO6H3Y/y/H0gwHICqM9YxRwLs6GJdkzoqJATFShM7PKJA==", - "requires": { - "cmd-shim": "^2.0.2", - "fs-extra": "^6.0.1", - "npmlog": "^4.1.2" + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/describe-ref": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.1.0.tgz", - "integrity": "sha512-0a7WFKDSmdEwwmEj+ZfhI7SkkG1CDcVhfW8VhPqr6gnCMY+ryt6iV/rR7ygb0eCDg8wEe9eQsiwbnrbXDLjIDw==", - "requires": { - "@lerna/child-process": "^3.0.0", - "npmlog": "^4.1.2" + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/diff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.1.3.tgz", - "integrity": "sha512-30G74DxdQC4dR3U0yqh5mjcioLDUmSy1ntntdF3khvKV9oiMVzCSOO0oOlSwIdmohke+bQ//oF+oyl0Dy1TUTQ==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/command": "^3.1.3", - "@lerna/validation-error": "^3.0.0", - "npmlog": "^4.1.2" + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "@lerna/exec": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.1.3.tgz", - "integrity": "sha512-r0yQj9Rza5a42Shts8rXYGU1/Va8hO2atk/TEZgrA1EcTwgZyAuXsuML5UWbC/eLgwEjVDmc3MUSj8O1JijBMQ==", - "requires": { - "@lerna/batch-packages": "^3.1.2", - "@lerna/child-process": "^3.0.0", - "@lerna/command": "^3.1.3", - "@lerna/filter-options": "^3.1.2", - "@lerna/run-parallel-batches": "^3.0.0", - "@lerna/validation-error": "^3.0.0" - } - }, - "@lerna/filter-options": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.1.2.tgz", - "integrity": "sha512-smbvSGK/eU+7PDKO4jbJ7XYO2XTfhnwPeOTuwSm1mlWS5dUGasYkhAuFzouFh60aZBvmW0e6APe0XYeofQNcCg==", - "requires": { - "@lerna/collect-updates": "^3.1.0", - "@lerna/filter-packages": "^3.0.0", - "dedent": "^0.7.0" + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/filter-packages": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.0.0.tgz", - "integrity": "sha512-zwbY1J4uRjWRZ/FgYbtVkq7I3Nduwsg2V2HwLKSzwV2vPglfGqgovYOVkND6/xqe2BHwDX4IyA2+e7OJmLaLSA==", - "requires": { - "@lerna/validation-error": "^3.0.0", - "multimatch": "^2.1.0", - "npmlog": "^4.1.2" + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/get-npm-exec-opts": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.0.0.tgz", - "integrity": "sha512-arcYUm+4xS8J3Palhl+5rRJXnZnFHsLFKHBxznkPIxjwGQeAEw7df38uHdVjEQ+HNeFmHnBgSqfbxl1VIw5DHg==", - "requires": { - "npmlog": "^4.1.2" + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/global-options": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.1.3.tgz", - "integrity": "sha512-LVeZU/Zgc0XkHdGMRYn+EmHfDmmYNwYRv3ta59iCVFXLVp7FRFWF7oB1ss/WRa9x/pYU0o6L8as/5DomLUGASA==" - }, - "@lerna/has-npm-version": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.0.4.tgz", - "integrity": "sha512-RisEWZBROi8corPb/PUIQqT+xGPLeriJ/n6VCeO6GROCO1fyYBX7kgFmVpFOytufWFkI04qBgLaUs+CEc8Yspg==", - "requires": { - "@lerna/child-process": "^3.0.0", - "semver": "^5.5.0" + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/import": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.1.3.tgz", - "integrity": "sha512-+XV/EHXEHbyMmprz8zQa0VF0TZ+txRIrcF3Q/PsuZ4isVxawIbP1CmgE0yn0/1XSNJwGKsuPfGauRtnjUi2LJw==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/command": "^3.1.3", - "@lerna/prompt": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "dedent": "^0.7.0", - "fs-extra": "^6.0.1", - "p-map-series": "^1.0.0" - } - }, - "@lerna/init": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.1.3.tgz", - "integrity": "sha512-c418p6fAfJ+b/tidB8/O/ABGLX7a5y5uJSWxH2/Mp7i5da/RD27XJ6E6818hGAbUbVQw04+XuXHtrWYlWLEJCw==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/command": "^3.1.3", - "fs-extra": "^6.0.1", - "p-map": "^1.2.0", - "write-json-file": "^2.3.0" - } - }, - "@lerna/link": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.1.4.tgz", - "integrity": "sha512-AAl4ctKtE6975zxdrsr16CAh/K4y0ZjjY9XDdD+zMxsSPELvRVG6M36O1A72AmKz5Nhh1l82bPrw7w54TbQ8hA==", - "requires": { - "@lerna/command": "^3.1.3", - "@lerna/package-graph": "^3.1.2", - "@lerna/symlink-dependencies": "^3.1.4", - "p-map": "^1.2.0", - "slash": "^1.0.0" - } - }, - "@lerna/list": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.1.3.tgz", - "integrity": "sha512-/ncX5Kj1ddLgZuBkjaoluJgcmAAm/L4/AymVNBgVrw6dOad0C0RB6oIcRAbxDennEQ25wSOFmuXRZHYHY9VYyg==", - "requires": { - "@lerna/command": "^3.1.3", - "@lerna/filter-options": "^3.1.2", - "@lerna/listable": "^3.0.0", - "@lerna/output": "^3.0.0" + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" } }, - "@lerna/listable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.0.0.tgz", - "integrity": "sha512-HX/9hyx1HLg2kpiKXIUc1EimlkK1T58aKQ7ovO7rQdTx9ForpefoMzyLnHE1n4XrUtEszcSWJIICJ/F898M6Ag==", - "requires": { - "chalk": "^2.3.1", - "columnify": "^1.5.4" + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" } }, - "@lerna/log-packed": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.0.4.tgz", - "integrity": "sha512-vVQHgMagE2wnbxhNY9nFkdu+Cx2TsyWalkJfkxbNzmo6gOCrDsxCBDj9vTEV8Q+4aWx0C0Bsc0sB2Eb8y/+ofA==", - "requires": { - "byte-size": "^4.0.3", - "columnify": "^1.5.4", - "has-unicode": "^2.0.1", - "npmlog": "^4.1.2" + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", + "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "@lerna/npm-conf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.0.0.tgz", - "integrity": "sha512-xXG7qt349t+xzaHTQELmIDjbq8Q49HOMR8Nx/gTDBkMl02Fno91LXFnA4A7ErPiyUSGqNSfLw+zgij0hgpeN7w==", - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/npm-dist-tag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.0.0.tgz", - "integrity": "sha512-ZOcfcsNJlCoVHvLOROdCTvqD3keG3TJ78Cu8sALsz8n0kEz2ga7tNy5wbQ67SGyY7+jq4YiBv5BwXjV+56Sv+A==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/get-npm-exec-opts": "^3.0.0", - "npmlog": "^4.1.2" + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/npm-install": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.0.0.tgz", - "integrity": "sha512-e0sspVUfzEKhqsRIxzWqZ/uMBHzZSzOa4HCeORErEZu+dmDoI145XYhqvCVn7EvbAb407FV2H9GVeoP0JeG8GQ==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/get-npm-exec-opts": "^3.0.0", - "fs-extra": "^6.0.1", - "npm-package-arg": "^6.0.0", - "npmlog": "^4.1.2", - "signal-exit": "^3.0.2", - "write-pkg": "^3.1.0" + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@lerna/npm-publish": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.2.0.tgz", - "integrity": "sha512-x13EGrjZk9w8gCQAE44aKbeO1xhLizLJ4tKjzZmQqKEaUCugF4UU8ZRGshPMRFBdsHTEWh05dkKx2oPMoaf0dw==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/get-npm-exec-opts": "^3.0.0", - "@lerna/has-npm-version": "^3.0.4", - "@lerna/log-packed": "^3.0.4", - "fs-extra": "^6.0.1", - "npmlog": "^4.1.2", - "p-map": "^1.2.0" + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", + "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-wrap-function": "^7.25.0", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@lerna/npm-run-script": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.0.0.tgz", - "integrity": "sha512-Y1H4Myer1S7an33FDK0eqyR+95PujUePC/xJZKq/H50SaQNwBw7KMlxXxy6kXVEcQhmvQsER4Bw3msgqwwGYIw==", - "requires": { - "@lerna/child-process": "^3.0.0", - "@lerna/get-npm-exec-opts": "^3.0.0", - "npmlog": "^4.1.2" + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@lerna/output": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.0.0.tgz", - "integrity": "sha512-EFxnSbO0zDEVKkTKpoCUAFcZjc3gn3DwPlyTDxbeqPU7neCfxP4rA4+0a6pcOfTlRS5kLBRMx79F2TRCaMM3DA==", - "requires": { - "npmlog": "^4.1.2" + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/package": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.0.0.tgz", - "integrity": "sha512-djzEJxzn212wS8d9znBnlXkeRlPL7GqeAYBykAmsuq51YGvaQK67Umh5ejdO0uxexF/4r7yRwgrlRHpQs8Rfqg==", - "requires": { - "npm-package-arg": "^6.0.0", - "write-pkg": "^3.1.0" + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", + "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/package-graph": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.1.2.tgz", - "integrity": "sha512-9wIWb49I1IJmyjPdEVZQ13IAi9biGfH/OZHOC04U2zXGA0GLiY+B3CAx6FQvqkZ8xEGfqzmXnv3LvZ0bQfc1aQ==", - "requires": { - "@lerna/validation-error": "^3.0.0", - "npm-package-arg": "^6.0.0", - "semver": "^5.5.0" + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/project": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.0.0.tgz", - "integrity": "sha512-XhDFVfqj79jG2Speggd15RpYaE8uiR25UKcQBDmumbmqvTS7xf2cvl2pq2UTvDafaJ0YwFF3xkxQZeZnFMwdkw==", - "requires": { - "@lerna/package": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "cosmiconfig": "^5.0.2", - "dedent": "^0.7.0", - "dot-prop": "^4.2.0", - "glob-parent": "^3.1.0", - "globby": "^8.0.1", - "load-json-file": "^4.0.0", - "npmlog": "^4.1.2", - "p-map": "^1.2.0", - "resolve-from": "^4.0.0", - "write-json-file": "^2.3.0" - } - }, - "@lerna/prompt": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.0.0.tgz", - "integrity": "sha512-EzvNexDTh//GlpOz68zRo16NdOIqWqiiXMs9tIxpELQubH+kUGKvBSiBrZ2Zyrfd8pQhIf+8qARtkCG+G7wzQQ==", - "requires": { - "inquirer": "^5.1.0", - "npmlog": "^4.1.2" + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@lerna/publish": { + "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.2.1.tgz", - "integrity": "sha512-SnSBstK/G9qLt5rS56pihNacgsu3UgxXiCexWb57GGEp2eDguQ7rFzxVs4JMQQWmVG97EMJQxfFV54tW2sqtIw==", - "requires": { - "@lerna/batch-packages": "^3.1.2", - "@lerna/check-working-tree": "^3.1.0", - "@lerna/child-process": "^3.0.0", - "@lerna/collect-updates": "^3.1.0", - "@lerna/command": "^3.1.3", - "@lerna/describe-ref": "^3.1.0", - "@lerna/get-npm-exec-opts": "^3.0.0", - "@lerna/npm-dist-tag": "^3.0.0", - "@lerna/npm-publish": "^3.2.0", - "@lerna/output": "^3.0.0", - "@lerna/prompt": "^3.0.0", - "@lerna/run-lifecycle": "^3.2.0", - "@lerna/run-parallel-batches": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "@lerna/version": "^3.2.0", - "fs-extra": "^6.0.1", - "npm-package-arg": "^6.0.0", - "npmlog": "^4.1.2", - "p-finally": "^1.0.0", - "p-map": "^1.2.0", - "p-pipe": "^1.2.0", - "p-reduce": "^1.0.0", - "semver": "^5.5.0" - } - }, - "@lerna/resolve-symlink": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.0.0.tgz", - "integrity": "sha512-MqjW9e+QVXts5IK5dk1XnYx7JKb+g+tQkOnnpAxYWHjahf3rGJ7Ru8maWh8KoPE+nIHAekk4WcjpiA9nLKvkFQ==", - "requires": { - "fs-extra": "^6.0.1", - "npmlog": "^4.1.2", - "read-cmd-shim": "^1.0.1" + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "@lerna/rimraf-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.0.0.tgz", - "integrity": "sha512-epvh/RGWSOYdrNgrizMcRq9VyCHkeY0LpIE4074r4ouKdYNhBT0LlpT0yMLvQgQKJkKRlqcfhJHvZeGHhXQyGg==", - "requires": { - "@lerna/child-process": "^3.0.0", - "npmlog": "^4.1.2", - "path-exists": "^3.0.0", - "rimraf": "^2.6.2" + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "@lerna/run": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.1.3.tgz", - "integrity": "sha512-O26WdR+sQFSG2Fpc67nw+m8oVq3R+H6jsscKuB6VJafU+V4/hPURSbuFZIcmnD9MLmzAIhlQiCf0Fy6s/1MPPA==", - "requires": { - "@lerna/batch-packages": "^3.1.2", - "@lerna/command": "^3.1.3", - "@lerna/filter-options": "^3.1.2", - "@lerna/npm-run-script": "^3.0.0", - "@lerna/output": "^3.0.0", - "@lerna/run-parallel-batches": "^3.0.0", - "@lerna/validation-error": "^3.0.0", - "p-map": "^1.2.0" - } - }, - "@lerna/run-lifecycle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.2.0.tgz", - "integrity": "sha512-kGGdHJRyeZF+VTtal1DBptg6qwIsOLg3pKtmRm1rCMNN7j4kgrA9L07ZoRar8LjQXvfuheB1LSKHd5d04pr4Tg==", - "requires": { - "@lerna/npm-conf": "^3.0.0", - "npm-lifecycle": "^2.0.0", - "npmlog": "^4.1.2" - } - }, - "@lerna/run-parallel-batches": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/run-parallel-batches/-/run-parallel-batches-3.0.0.tgz", - "integrity": "sha512-Mj1ravlXF7AkkewKd9YFq9BtVrsStNrvVLedD/b2wIVbNqcxp8lS68vehXVOzoL/VWNEDotvqCQtyDBilCodGw==", - "requires": { - "p-map": "^1.2.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/symlink-binary": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.1.4.tgz", - "integrity": "sha512-uQ8pYxzygahshXJAeC/vY4eSi+kFM0S2Pi15hJsJI3W7Ec6ysSYU1lXemb6kqoIqkTDJZWnfKXEq/3FLE+JYhg==", - "requires": { - "@lerna/create-symlink": "^3.0.0", - "@lerna/package": "^3.0.0", - "fs-extra": "^6.0.1", - "p-map": "^1.2.0", - "read-pkg": "^3.0.0" - } - }, - "@lerna/symlink-dependencies": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.1.4.tgz", - "integrity": "sha512-iModwb0Xh0N0t55C6S4K2mzLdu1zXVsBc0qubUY1x0RSul92z8NeAe1aM5JzwMzuSoMA/LRiD1lNMWMRBf4JVg==", - "requires": { - "@lerna/create-symlink": "^3.0.0", - "@lerna/resolve-symlink": "^3.0.0", - "@lerna/symlink-binary": "^3.1.4", - "fs-extra": "^6.0.1", - "p-finally": "^1.0.0", - "p-map": "^1.2.0", - "p-map-series": "^1.0.0" - } - }, - "@lerna/validation-error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.0.0.tgz", - "integrity": "sha512-5wjkd2PszV0kWvH+EOKZJWlHEqCTTKrWsvfHnHhcUaKBe/NagPZFWs+0xlsDPZ3DJt5FNfbAPAnEBQ05zLirFA==", - "requires": { - "npmlog": "^4.1.2" - } - }, - "@lerna/version": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.2.0.tgz", - "integrity": "sha512-1AVDMpeecSMiG1cacduE+f2KO0mC7F/9MvWsHtp+rjkpficMcsVme7IMtycuvu/F07wY4Xr9ioFKYTwTcybbIA==", - "requires": { - "@lerna/batch-packages": "^3.1.2", - "@lerna/check-working-tree": "^3.1.0", - "@lerna/child-process": "^3.0.0", - "@lerna/collect-updates": "^3.1.0", - "@lerna/command": "^3.1.3", - "@lerna/conventional-commits": "^3.0.2", - "@lerna/output": "^3.0.0", - "@lerna/prompt": "^3.0.0", - "@lerna/run-lifecycle": "^3.2.0", - "@lerna/validation-error": "^3.0.0", - "chalk": "^2.3.1", - "dedent": "^0.7.0", - "minimatch": "^3.0.4", - "npmlog": "^4.1.2", - "p-map": "^1.2.0", - "p-pipe": "^1.2.0", - "p-reduce": "^1.0.0", - "p-waterfall": "^1.0.0", - "semver": "^5.5.0", - "slash": "^1.0.0", - "temp-write": "^3.4.0" + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" } }, - "@lerna/write-log-file": { + "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.0.0.tgz", - "integrity": "sha512-SfbPp29lMeEVOb/M16lJwn4nnx5y+TwCdd7Uom9umd7KcZP0NOvpnX0PHehdonl7TyHZ1Xx2maklYuCLbQrd/A==", - "requires": { - "npmlog": "^4.1.2", - "write-file-atomic": "^2.3.0" + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" } }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "@nodelib/fs.stat": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.1.tgz", - "integrity": "sha512-KU/VDjC5RwtDUZiz3d+DHXJF2lp5hB9dn552TXIyptj8SH1vXmR40mG0JgGq03IlYsOgGfcv8xrLpSQ0YUMQdA==" + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } }, - "JSONStream": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz", - "integrity": "sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", + "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", + "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } }, - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", + "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "agentkeepalive": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.1.tgz", - "integrity": "sha512-Cte/sTY9/XcygXjJ0q58v//SnEQ7ViWExKyJpLJlLqomDbQyMLh6Is4KuWJ/wmxzhiwkGRple7Gqv1zf6Syz5w==", - "requires": { - "humanize-ms": "^1.2.1" + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", + "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.24.7.tgz", + "integrity": "sha512-CcmFwUJ3tKhLjPdt4NP+SHMshebytF8ZTYOv5ZDpkzq2sin80Wb5vJrGt8fhPrORQCfoSa0LAxC/DW+GAC5+Hw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.24.7.tgz", + "integrity": "sha512-bTPz4/635WQ9WhwsyPdxUJDVpsi/X9BMmy/8Rf/UAlOO4jSql4CxUCjWI5PiM+jG+c4LVPTScoTw80geFj9+Bw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", + "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=" + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "asap": { + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", + "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", + "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", + "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/traverse": "^7.25.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", + "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.2.tgz", + "integrity": "sha512-InBZ0O8tew5V0K6cHcQ+wgxlrjOw1W4wDXLkOTjLRD8GYhTSkxTVBtdy3MMtvYBrbAWa1Qm3hNoTc1620Yj+Mg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-flow": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", + "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", + "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", + "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz", + "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz", + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.7.tgz", + "integrity": "sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-flow-strip-types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", + "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", + "dev": true, + "peer": true, + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register/node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "peer": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "peer": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/register/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/register/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "peer": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/register/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true, + "peer": true + }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dev": true, + "peer": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "peer": true + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "peer": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "peer": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz", + "integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==", + "dev": true, + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native-community/cli": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-14.0.0.tgz", + "integrity": "sha512-KwMKJB5jsDxqOhT8CGJ55BADDAYxlYDHv5R/ASQlEcdBEZxT0zZmnL0iiq2VqzETUy+Y/Nop+XDFgqyoQm0C2w==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-clean": "14.0.0", + "@react-native-community/cli-config": "14.0.0", + "@react-native-community/cli-debugger-ui": "14.0.0", + "@react-native-community/cli-doctor": "14.0.0", + "@react-native-community/cli-server-api": "14.0.0", + "@react-native-community/cli-tools": "14.0.0", + "@react-native-community/cli-types": "14.0.0", + "chalk": "^4.1.2", + "commander": "^9.4.1", + "deepmerge": "^4.3.0", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.1.3", + "prompts": "^2.4.2", + "semver": "^7.5.2" + }, + "bin": { + "rnc-cli": "build/bin.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native-community/cli-clean": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-14.0.0.tgz", + "integrity": "sha512-kvHthZTNur/wLLx8WL5Oh+r04zzzFAX16r8xuaLhu9qGTE6Th1JevbsIuiQb5IJqD8G/uZDKgIZ2a0/lONcbJg==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "14.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-config": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-14.0.0.tgz", + "integrity": "sha512-2Nr8KR+dgn1z+HLxT8piguQ1SoEzgKJnOPQKE1uakxWaRFcQ4LOXgzpIAscYwDW6jmQxdNqqbg2cRUoOS7IMtQ==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "14.0.0", + "chalk": "^4.1.2", + "cosmiconfig": "^9.0.0", + "deepmerge": "^4.3.0", + "fast-glob": "^3.3.2", + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli-debugger-ui": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-14.0.0.tgz", + "integrity": "sha512-JpfzILfU7eKE9+7AMCAwNJv70H4tJGVv3ZGFqSVoK1YHg5QkVEGsHtoNW8AsqZRS6Fj4os+Fmh+r+z1L36sPmg==", + "dev": true, + "peer": true, + "dependencies": { + "serve-static": "^1.13.1" + } + }, + "node_modules/@react-native-community/cli-doctor": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-14.0.0.tgz", + "integrity": "sha512-in6jylHjaPUaDzV+JtUblh8m9JYIHGjHOf6Xn57hrmE5Zwzwuueoe9rSMHF1P0mtDgRKrWPzAJVejElddfptWA==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-config": "14.0.0", + "@react-native-community/cli-platform-android": "14.0.0", + "@react-native-community/cli-platform-apple": "14.0.0", + "@react-native-community/cli-platform-ios": "14.0.0", + "@react-native-community/cli-tools": "14.0.0", + "chalk": "^4.1.2", + "command-exists": "^1.2.8", + "deepmerge": "^4.3.0", + "envinfo": "^7.13.0", + "execa": "^5.0.0", + "node-stream-zip": "^1.9.1", + "ora": "^5.4.1", + "semver": "^7.5.2", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1", + "yaml": "^2.2.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native-community/cli-platform-android": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-14.0.0.tgz", + "integrity": "sha512-nt7yVz3pGKQXnVa5MAk7zR+1n41kNKD3Hi2OgybH5tVShMBo7JQoL2ZVVH6/y/9wAwI/s7hXJgzf1OIP3sMq+Q==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "14.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.2.4", + "logkitty": "^0.7.1" + } + }, + "node_modules/@react-native-community/cli-platform-apple": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-apple/-/cli-platform-apple-14.0.0.tgz", + "integrity": "sha512-WniJL8vR4MeIsjqio2hiWWuUYUJEL3/9TDL5aXNwG68hH3tYgK3742+X9C+vRzdjTmf5IKc/a6PwLsdplFeiwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-tools": "14.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.2.4", + "ora": "^5.4.1" + } + }, + "node_modules/@react-native-community/cli-platform-ios": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-14.0.0.tgz", + "integrity": "sha512-8kxGv7mZ5nGMtueQDq+ndu08f0ikf3Zsqm3Ix8FY5KCXpSgP14uZloO2GlOImq/zFESij+oMhCkZJGggpWpfAw==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-platform-apple": "14.0.0" + } + }, + "node_modules/@react-native-community/cli-server-api": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-14.0.0.tgz", + "integrity": "sha512-A0FIsj0QCcDl1rswaVlChICoNbfN+mkrKB5e1ab5tOYeZMMyCHqvU+eFvAvXjHUlIvVI+LbqCkf4IEdQ6H/2AQ==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-debugger-ui": "14.0.0", + "@react-native-community/cli-tools": "14.0.0", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-native-community/cli-server-api/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, + "node_modules/@react-native-community/cli-server-api/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native-community/cli-tools": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-14.0.0.tgz", + "integrity": "sha512-L7GX5hyYYv0ZWbAyIQKzhHuShnwDqlKYB0tqn57wa5riGCaxYuRPTK+u4qy+WRCye7+i8M4Xj6oQtSd4z0T9cA==", + "dev": true, + "peer": true, + "dependencies": { + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "mime": "^2.4.1", + "open": "^6.2.0", + "ora": "^5.4.1", + "semver": "^7.5.2", + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" + } + }, + "node_modules/@react-native-community/cli-types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-14.0.0.tgz", + "integrity": "sha512-CMUevd1pOWqvmvutkUiyQT2lNmMHUzSW7NKc1xvHgg39NjbS58Eh2pMzIUP85IwbYNeocfYc3PH19vA/8LnQtg==", + "dev": true, + "peer": true, + "dependencies": { + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/netinfo": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.3.2.tgz", + "integrity": "sha512-YsaS3Dutnzqd1BEoeC+DEcuNJedYRkN6Ef3kftT5Sm8ExnCF94C/nl4laNxuvFli3+Jz8Df3jO25Jn8A9S0h4w==", + "dev": true, + "peerDependencies": { + "react-native": ">=0.59" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.2.tgz", + "integrity": "sha512-P1dLHjpUeC0AIkDHRYcx0qLMr+p92IPWL3pmczzo6T76Qa9XzruQOYy0jittxyBK91Csn6HHQ/eit8TeXW8MVw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.2.tgz", + "integrity": "sha512-BIKVh2ZJPkzluUGgCNgpoh6NTHgX8j04FCS0Z/rTmRJ66hir/EUBl8frMFKrOy/6i4VvZEltOWB5eWfHe1AYgw==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native/codegen": "0.75.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.2.tgz", + "integrity": "sha512-mprpsas+WdCEMjQZnbDiAC4KKRmmLbMB+o/v4mDqKlH4Mcm7RdtP5t80MZGOVCHlceNp1uEIpXywx69DNwgbgg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-default-from": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.18.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-syntax-optional-chaining": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.20.0", + "@babel/plugin-transform-block-scoping": "^7.0.0", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-classes": "^7.0.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.20.0", + "@babel/plugin-transform-flow-strip-types": "^7.20.0", + "@babel/plugin-transform-for-of": "^7.0.0", + "@babel/plugin-transform-function-name": "^7.0.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.0.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.5", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.5", + "@babel/plugin-transform-parameters": "^7.0.0", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0", + "@babel/plugin-transform-regenerator": "^7.20.0", + "@babel/plugin-transform-runtime": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.5.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "@babel/template": "^7.0.0", + "@react-native/babel-plugin-codegen": "0.75.2", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.2.tgz", + "integrity": "sha512-OkWdbtO2jTkfOXfj3ibIL27rM6LoaEuApOByU2G8X+HS6v9U87uJVJlMIRWBDmnxODzazuHwNVA2/wAmSbucaw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.0", + "glob": "^7.1.1", + "hermes-parser": "0.22.0", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.2.tgz", + "integrity": "sha512-/tz0bzVja4FU0aAimzzQ7iYR43peaD6pzksArdrrGhlm8OvFYAQPOYSNeIQVMSarwnkNeg1naFKaeYf1o3++yA==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-server-api": "14.0.0-alpha.11", + "@react-native-community/cli-tools": "14.0.0-alpha.11", + "@react-native/dev-middleware": "0.75.2", + "@react-native/metro-babel-transformer": "0.75.2", + "chalk": "^4.0.0", + "execa": "^5.1.1", + "metro": "^0.80.3", + "metro-config": "^0.80.3", + "metro-core": "^0.80.3", + "node-fetch": "^2.2.0", + "querystring": "^0.2.1", + "readline": "^1.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native-community/cli-debugger-ui": { + "version": "14.0.0-alpha.11", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-14.0.0-alpha.11.tgz", + "integrity": "sha512-0wCNQxhCniyjyMXgR1qXliY180y/2QbvoiYpp2MleGQADr5M1b8lgI4GoyADh5kE+kX3VL0ssjgyxpmbpCD86A==", + "dev": true, + "peer": true, + "dependencies": { + "serve-static": "^1.13.1" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native-community/cli-server-api": { + "version": "14.0.0-alpha.11", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-14.0.0-alpha.11.tgz", + "integrity": "sha512-I7YeYI7S5wSxnQAqeG8LNqhT99FojiGIk87DU0vTp6U8hIMLcA90fUuBAyJY38AuQZ12ZJpGa8ObkhIhWzGkvg==", + "dev": true, + "peer": true, + "dependencies": { + "@react-native-community/cli-debugger-ui": "14.0.0-alpha.11", + "@react-native-community/cli-tools": "14.0.0-alpha.11", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "pretty-format": "^26.6.2", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native-community/cli-tools": { + "version": "14.0.0-alpha.11", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-14.0.0-alpha.11.tgz", + "integrity": "sha512-HQCfVnX9aqRdKdLxmQy4fUAUo+YhNGlBV7ZjOayPbuEGWJ4RN+vSy0Cawk7epo7hXd6vKzc7P7y3HlU6Kxs7+w==", + "dev": true, + "peer": true, + "dependencies": { + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "mime": "^2.4.1", + "open": "^6.2.0", + "ora": "^5.4.1", + "semver": "^7.5.2", + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.2.tgz", + "integrity": "sha512-qIC6mrlG8RQOPaYLZQiJwqnPchAVGnHWcVDeQxPMPLkM/D5+PC8tuKWYOwgLcEau3RZlgz7QQNk31Qj2/OJG6Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.2.tgz", + "integrity": "sha512-fTC5m2uVjYp1XPaIJBFgscnQjPdGVsl96z/RfLgXDq0HBffyqbg29ttx6yTCx7lIa9Gdvf6nKQom+e+Oa4izSw==", + "dev": true, + "peer": true, + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.75.2", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "node-fetch": "^2.2.0", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "ws": "^6.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "peer": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.2.tgz", + "integrity": "sha512-AELeAOCZi3B2vE6SeN+mjpZjjqzqa76yfFBB3L3f3NWiu4dm/YClTGOj+5IVRRgbt8LDuRImhDoaj7ukheXr4Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.2.tgz", + "integrity": "sha512-AtLd3mbiE+FXK2Ru3l2NFOXDhUvzdUsCP4qspUw0haVaO/9xzV97RVD2zz0lur2f/LmZqQ2+KXyYzr7048b5iw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.2.tgz", + "integrity": "sha512-EygglCCuOub2sZ00CSIiEekCXoGL2XbOC6ssOB47M55QKvhdPG/0WBQXvmOmiN42uZgJK99Lj749v4rB0PlPIQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@react-native/babel-preset": "0.75.2", + "hermes-parser": "0.22.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.2.tgz", + "integrity": "sha512-nPwWJFtsqNFS/qSG9yDOiSJ64mjG7RCP4X/HXFfyWzCM1jq49h/DYBdr+c3e7AvTKGIdy0gGT3vgaRUHZFVdUQ==", + "dev": true, + "peer": true + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.2.tgz", + "integrity": "sha512-pD5SVCjxc8k+JdoyQ+IlulBTEqJc3S4KUKsmv5zqbNCyETB0ZUvd4Su7bp+lLF6ALxx6KKmbGk8E3LaWEjUFFQ==", + "dev": true, + "peer": true, + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.1.0.tgz", + "integrity": "sha512-Ycr12N3ZPN96Fw2STurD21jMqzKwL9QuFhms3SD7KKRK7oaXUsBU9Zt0jL/rOPHiPYisI21/rXGO3jr9BnLHUA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.0.8", + "commondir": "^1.0.1", + "estree-walker": "^1.0.1", + "glob": "^7.1.2", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", + "integrity": "sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.0.8", + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.14.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "peer": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "peer": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "peer": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.14", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.14.tgz", + "integrity": "sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true, + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "node_modules/@types/nise": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@types/nise/-/nise-1.4.1.tgz", + "integrity": "sha512-LWDwHYO1C3YPpIQWXHeXAVih2nLsgN1Q5RamkYZRIZYfsz8HGNRji8vNhHs54LjcSgVx6AJC/6n/Q3Tn+fUb3g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.17.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.18.tgz", + "integrity": "sha512-/4QOuy3ZpV7Ya1GTRz5CYSz3DgkKpyUptXuQ5PPce7uuyJAOR7r9FhkmxJfvcNUXyklbC63a+YvB3jxy7s9ngw==", + "dev": true + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true, + "peer": true + }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-4sOxS3ZWXC0uHJLYcWAaLMxTvjRX3hT96eF4YWUh1ovTaenvibaZOE5uXtIp4mksKMLRwo7YDiCBCw6vBiUPVg==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true, + "peer": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-istanbul": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.9.tgz", + "integrity": "sha512-vdYE4FkC/y2lxcN3Dcj54Bw+ericmDwiex0B8LV5F/YNYEYP1mgVwhPnHwWGAXu38qizkjOuyczKbFTALfzFKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@istanbuljs/schema": "^0.1.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-instrument": "^6.0.3", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magicast": "^0.3.5", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.9" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-istanbul/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vitest/coverage-istanbul/node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/expect/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/@vitest/expect/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@vitest/expect/node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "peer": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "dev": true, + "peer": true + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-fragments/-/ansi-fragments-0.2.1.tgz", + "integrity": "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==", + "dev": true, + "peer": true, + "dependencies": { + "colorette": "^1.0.7", + "slice-ansi": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/ansi-fragments/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdirsjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", + "integrity": "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==", + "dev": true, + "peer": true + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "peer": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "peer": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true, + "peer": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "dev": true, + "peer": true, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/browserstack": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.3.tgz", + "integrity": "sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg==", + "dev": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + } + }, + "node_modules/browserstack-local": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.4.tgz", + "integrity": "sha512-OueHCaQQutO+Fezg+ZTieRn+gdV+JocLjiAQ8nYecu08GhIt3ms79cDHfpoZmECAdoQ6OLdm7ODd+DtQzl4lrA==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "https-proxy-agent": "^5.0.1", + "is-running": "^2.1.0", + "ps-tree": "=1.2.0", + "temp-fs": "^0.9.9" + } + }, + "node_modules/browserstack/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/browserstack/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/browserstack/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caching-transform/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/caching-transform/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "dev": true, + "peer": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-callsite/node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "dev": true, + "peer": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", + "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "peer": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "peer": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "dev": true, + "peer": true + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "peer": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "dev": true, + "peer": true, + "dependencies": { + "browserslist": "^4.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "peer": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "peer": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "peer": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/coveralls-next": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-4.2.0.tgz", + "integrity": "sha512-zg41a/4QDSASPtlV6gp+6owoU43U5CguxuPZR3nPZ26M5ZYdEK3MdUe7HwE+AnCZPkucudfhqqJZehCNkz2rYg==", + "dev": true, + "dependencies": { + "form-data": "4.0.0", + "js-yaml": "4.1.0", + "lcov-parse": "1.0.0", + "log-driver": "1.2.7", + "minimist": "1.2.7" + }, + "bin": { + "coveralls": "bin/coveralls.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/coveralls-next/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/coveralls-next/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/coveralls-next/node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz", + "integrity": "sha512-6IvPrADQyyPGLpMnUh6kfKiqy7SrbXbjoUuZ90WMBJKErzv2pCiwlGEXjRX9/54OnTq+XFVnkOnOMzclLI5aEA==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "peer": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denodeify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", + "integrity": "sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==", + "dev": true, + "peer": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz", + "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "dev": true, + "peer": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "peer": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "dev": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dev": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", + "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=5.0.0", + "prettier": ">=1.13.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true, + "peer": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "peer": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-cache-dir/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "dev": true, + "peer": true + }, + "node_modules/flow-parser": { + "version": "0.244.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.244.0.tgz", + "integrity": "sha512-Dkc88m5k8bx1VvHTO9HEJ7tvMcSb3Zvcv1PY4OHK7pHdtdY2aUjhmPy6vpjVJ2uUUOIybRlb91sXE8g4doChtA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formatio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "integrity": "sha512-YAF05v8+XCxAyHOdiiAmHdgCVPrWO8X744fYIPtBciIorh5LndWfi1gjeJ16sTbJhzek9kd+j3YByhohtz5Wmg==", + "deprecated": "This package is unmaintained. Use @sinonjs/formatio instead", + "dev": true, + "dependencies": { + "samsam": "1.x" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha512-05cXDIwNbFaoFWaz5gNHlUTbH5whiss/hr/ibzPd4MH3cR4w0ZKeIPiVdbyJurg3O5r/Bjpvn9KOb1/rPMf3nA==", + "dev": true, + "dependencies": { + "null-check": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/happy-dom": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-16.6.0.tgz", + "integrity": "sha512-Zz5S9sog8a3p8XYZbO+eI1QMOAvCNnIoyrH8A8MLX+X2mJrzADTy+kdETmc4q+uD9AGAvQYGn96qBAn2RAciKw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hermes-estree": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.22.0.tgz", + "integrity": "sha512-FLBt5X9OfA8BERUdc6aZS36Xz3rRuB0Y/mfocSADWEJfomc1xfene33GdyAmtTkKTBXTN/EgAy+rjTKkkZJHlw==", + "dev": true, + "peer": true + }, + "node_modules/hermes-parser": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.22.0.tgz", + "integrity": "sha512-gn5RfZiEXCsIWsFGsKiykekktUoh0PdFWYocXsUdZIyWSckT6UIyPcyyUIPSR3kpnELWeK3n3ztAse7Mat6PSA==", + "dev": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.22.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dev": true, + "peer": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" } }, - "assert-plus": { + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } }, - "assign-symbols": { + "node_modules/is-module": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==", + "dev": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "balanced-match": { + "node_modules/is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "peer": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.1.tgz", + "integrity": "sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "peer": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-android": { + "version": "250231.0.0", + "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", + "integrity": "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==", + "dev": true, + "peer": true + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "dev": true, + "peer": true + }, + "node_modules/jscodeshift": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.14.0.tgz", + "integrity": "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.13.16", + "@babel/parser": "^7.13.16", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/preset-flow": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "babel-core": "^7.0.0-bridge.0", + "chalk": "^4.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.21.0", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/jscodeshift/node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "peer": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-browserstack-launcher": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.6.0.tgz", + "integrity": "sha512-Y/UWPdHZkHIVH2To4GWHCTzmrsB6H7PBWy6pw+TWz5sr4HW2mcE+Uj6qWgoVNxvQU1Pfn5LQQzI6EQ65p8QbiQ==", + "dev": true, + "dependencies": { + "browserstack": "~1.5.1", + "browserstack-local": "^1.3.7", + "q": "~1.5.0" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-chai": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", + "integrity": "sha512-mqKCkHwzPMhgTYca10S90aCEX9+HjVjjrBFAsw36Zj7BlQNbokXXCAe6Ji04VUMsxcY5RLP7YphpfO06XOubdg==", + "dev": true, + "peerDependencies": { + "chai": "*", + "karma": ">=0.10.9" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", + "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "dev": true, + "dependencies": { + "fs-access": "^1.0.0", + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3" + } + }, + "node_modules/karma-webpack": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.1.tgz", + "integrity": "sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^9.0.3", + "webpack-merge": "^4.1.5" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/karma-webpack/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/karma-webpack/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } + { + "type": "paypal", + "url": "https://paypal.me/faisalman" }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } + ], + "engines": { + "node": "*" } }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "~2.0.0" + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" } }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", + "dev": true, + "bin": { + "lcov-parse": "bin/cli.js" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" } }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "builtins": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "peer": true }, - "byte-size": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-4.0.3.tgz", - "integrity": "sha512-JGC3EV2bCzJH/ENSh3afyJrH4vwxbHTuO5ljLoI5+2iJOcEpMgP8T782jH9b5qGxf2mSUIp1lfGnfKNrRHpvVg==" - }, - "cacache": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.2.0.tgz", - "integrity": "sha512-IFWl6lfK6wSeYCHUXh+N1lY72UDrpyrYQJNIVQf48paDuWbv5RbAtJYf/4gUQFObTCHZwdZ5sI8Iw7nqwP6nlQ==", - "requires": { - "bluebird": "^3.5.1", - "chownr": "^1.0.1", - "figgy-pudding": "^3.1.0", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "lru-cache": "^4.1.3", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^6.0.0", - "unique-filename": "^1.1.0", - "y18n": "^4.0.0" - } + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "optional": true + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "peer": true }, - "camelcase-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", - "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", - "requires": { - "camelcase": "^4.1.0", - "map-obj": "^2.0.0", - "quick-lru": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } + "node_modules/log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true, + "engines": { + "node": ">=0.8.6" } }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" } }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "node_modules/logkitty": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", + "integrity": "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-fragments": "^0.2.1", + "dayjs": "^1.8.15", + "yargs": "^15.1.0" + }, + "bin": { + "logkitty": "bin/logkitty.js" } }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" + "node_modules/logkitty/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } }, - "chownr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + "node_modules/logkitty/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "ci-info": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.4.0.tgz", - "integrity": "sha512-Oqmw2pVfCl8sCL+1QgMywPfdxPJPkC51y4usw0iiE2S9qnEOAqXy8bwl1CpMpnoU39g4iKJTz6QZj+28FvOnjQ==" - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } + "node_modules/logkitty/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" + "node_modules/logkitty/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + "node_modules/logkitty/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" + "node_modules/logkitty/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "peer": true + }, + "node_modules/logkitty/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "peer": true, "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "optional": true - } + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" } }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "peer": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } }, - "cmd-shim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-2.0.2.tgz", - "integrity": "sha1-b8vamUg6j9FdfTChlspp1oii79s=", - "requires": { - "graceful-fs": "^4.1.2", - "mkdirp": "~0.5.0" + "node_modules/lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "deprecated": "Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } }, - "columnify": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", - "requires": { - "strip-ansi": "^3.0.0", - "wcwidth": "^1.0.0" + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "peer": true, + "dependencies": { + "tmpl": "1.0.5" } }, - "compare-func": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", - "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", - "requires": { - "array-ify": "^1.0.0", - "dot-prop": "^3.0.0" - }, - "dependencies": { - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "requires": { - "is-obj": "^1.0.0" - } - } + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true, + "peer": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "dev": true, + "peer": true }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "dev": true, + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" } }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/metro": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.80.10.tgz", + "integrity": "sha512-FDPi0X7wpafmDREXe1lgg3WzETxtXh6Kpq8+IwsG35R2tMyp2kFIqDdshdohuvDt1J/qDARcEPq7V/jElTb1kA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/parser": "^7.20.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "denodeify": "^1.2.1", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.23.0", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.6.3", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.80.10", + "metro-cache": "0.80.10", + "metro-cache-key": "0.80.10", + "metro-config": "0.80.10", + "metro-core": "0.80.10", + "metro-file-map": "0.80.10", + "metro-resolver": "0.80.10", + "metro-runtime": "0.80.10", + "metro-source-map": "0.80.10", + "metro-symbolicate": "0.80.10", + "metro-transform-plugins": "0.80.10", + "metro-transform-worker": "0.80.10", + "mime-types": "^2.1.27", + "node-fetch": "^2.2.0", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "strip-ansi": "^6.0.0", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18" } }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "conventional-changelog-angular": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", - "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", - "requires": { - "compare-func": "^1.3.1", - "q": "^1.5.1" - } - }, - "conventional-changelog-core": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.11.tgz", - "integrity": "sha512-HvTE6RlqeEZ/NFPtQeFLsIDOLrGP3bXYr7lFLMhCVsbduF1MXIe8OODkwMFyo1i9ku9NWBwVnVn0jDmIFXjDRg==", - "requires": { - "conventional-changelog-writer": "^3.0.9", - "conventional-commits-parser": "^2.1.7", - "dateformat": "^3.0.0", - "get-pkg-repo": "^1.0.0", - "git-raw-commits": "^1.3.6", - "git-remote-origin-url": "^2.0.0", - "git-semver-tags": "^1.3.6", - "lodash": "^4.2.1", - "normalize-package-data": "^2.3.5", - "q": "^1.5.1", - "read-pkg": "^1.1.0", - "read-pkg-up": "^1.0.1", - "through2": "^2.0.0" - }, - "dependencies": { - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - } + "node_modules/metro-babel-transformer": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.80.10.tgz", + "integrity": "sha512-GXHueUzgzcazfzORDxDzWS9jVVRV6u+cR6TGvHOfGdfLzJCj7/D0PretLfyq+MwN20twHxLW+BUXkoaB8sCQBg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.23.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.0.tgz", + "integrity": "sha512-Rkp0PNLGpORw4ktsttkVbpYJbrYKS3hAnkxu8D9nvQi6LvSbuPa+tYw/t2u3Gjc35lYd/k95YkjqyTcN4zspag==", + "dev": true, + "peer": true + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.0.tgz", + "integrity": "sha512-xLwM4ylfHGwrm+2qXfO1JT/fnqEDGSnpS/9hQ4VLtqTexSviu2ZpBgz07U8jVtndq67qdb/ps0qvaWDZ3fkTyg==", + "dev": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.23.0" } }, - "conventional-changelog-preset-loader": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.8.tgz", - "integrity": "sha512-MkksM4G4YdrMlT2MbTsV2F6LXu/hZR0Tc/yenRrDIKRwBl/SP7ER4ZDlglqJsCzLJi4UonBc52Bkm5hzrOVCcw==" - }, - "conventional-changelog-writer": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.9.tgz", - "integrity": "sha512-n9KbsxlJxRQsUnK6wIBRnARacvNnN4C/nxnxCkH+B/R1JS2Fa+DiP1dU4I59mEDEjgnFaN2+9wr1P1s7GYB5/Q==", - "requires": { - "compare-func": "^1.3.1", - "conventional-commits-filter": "^1.1.6", - "dateformat": "^3.0.0", - "handlebars": "^4.0.2", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "semver": "^5.5.0", - "split": "^1.0.0", - "through2": "^2.0.0" - } - }, - "conventional-commits-filter": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.6.tgz", - "integrity": "sha512-KcDgtCRKJCQhyk6VLT7zR+ZOyCnerfemE/CsR3iQpzRRFbLEs0Y6rwk3mpDvtOh04X223z+1xyJ582Stfct/0Q==", - "requires": { - "is-subset": "^0.1.1", - "modify-values": "^1.0.0" - } - }, - "conventional-commits-parser": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", - "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", - "requires": { - "JSONStream": "^1.0.4", - "is-text-path": "^1.0.0", - "lodash": "^4.2.1", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0", - "trim-off-newlines": "^1.0.0" - } - }, - "conventional-recommended-bump": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-2.0.9.tgz", - "integrity": "sha512-YE6/o+648qkX3fTNvfBsvPW3tSnbZ6ec3gF0aBahCPgyoVHU2Mw0nUAZ1h1UN65GazpORngrgRC8QCltNYHPpQ==", - "requires": { - "concat-stream": "^1.6.0", - "conventional-changelog-preset-loader": "^1.1.8", - "conventional-commits-filter": "^1.1.6", - "conventional-commits-parser": "^2.1.7", - "git-raw-commits": "^1.3.6", - "git-semver-tags": "^1.3.6", - "meow": "^4.0.0", - "q": "^1.5.1" - } - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" + "node_modules/metro-cache": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.80.10.tgz", + "integrity": "sha512-8CBtDJwMguIE5RvV3PU1QtxUG8oSSX54mIuAbRZmcQ0MYiOl9JdrMd4JCBvIyhiZLoSStph425SMyCSnjtJsdA==", + "dev": true, + "peer": true, + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "metro-core": "0.80.10" + }, + "engines": { + "node": ">=18" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "node_modules/metro-cache-key": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.80.10.tgz", + "integrity": "sha512-57qBhO3zQfoU/hP4ZlLW5hVej2jVfBX6B4NcSfMj4LgDPL3YknWg80IJBxzQfjQY/m+fmMLmPy8aUMHzUp/guA==", + "dev": true, + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18" + } }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "node_modules/metro-config": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.80.10.tgz", + "integrity": "sha512-0GYAw0LkmGbmA81FepKQepL1KU/85Cyv7sAiWm6QWeV6AcVCpsKg6jGLqGHJ0LLPL60rWzA4TV1DQAlzdJAEtA==", + "dev": true, + "peer": true, + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.6.3", + "metro": "0.80.10", + "metro-cache": "0.80.10", + "metro-core": "0.80.10", + "metro-runtime": "0.80.10" + }, + "engines": { + "node": ">=18" + } }, - "cosmiconfig": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz", - "integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==", - "requires": { + "node_modules/metro-config/node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "peer": true, + "dependencies": { + "import-fresh": "^2.0.0", "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", + "js-yaml": "^3.13.1", "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" } }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "node_modules/metro-config/node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "dev": true, + "peer": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "requires": { - "array-find-index": "^1.0.1" + "node_modules/metro-config/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "peer": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" } }, - "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" - }, - "dargs": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", - "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", - "requires": { - "number-is-nan": "^1.0.0" + "node_modules/metro-config/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-core": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.80.10.tgz", + "integrity": "sha512-nwBB6HbpGlNsZMuzxVqxqGIOsn5F3JKpsp8PziS7Z4mV8a/jA1d44mVOgYmDa2q5WlH5iJfRIIhdz24XRNDlLA==", + "dev": true, + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.80.10" + }, + "engines": { + "node": ">=18" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" + "node_modules/metro-file-map": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.80.10.tgz", + "integrity": "sha512-ytsUq8coneaN7ZCVk1IogojcGhLIbzWyiI2dNmw2nnBgV/0A+M5WaTTgZ6dJEz3dzjObPryDnkqWPvIGLCPtiw==", + "dev": true, + "peer": true, + "dependencies": { + "anymatch": "^3.0.3", + "debug": "^2.2.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.6.3", + "micromatch": "^4.0.4", + "node-abort-controller": "^3.1.1", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" - }, - "debug": { + "node_modules/metro-file-map/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { + "dev": true, + "peer": true, + "dependencies": { "ms": "2.0.0" } }, - "debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, + "node_modules/metro-file-map/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/metro-minify-terser": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.80.10.tgz", + "integrity": "sha512-Xyv9pEYpOsAerrld7cSLIcnCCpv8ItwysOmTA+AKf1q4KyE9cxrH2O2SA0FzMCkPzwxzBWmXwHUr+A89BpEM6g==", + "dev": true, + "peer": true, "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - } + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18" } }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", - "requires": { - "clone": "^1.0.2" + "node_modules/metro-resolver": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.80.10.tgz", + "integrity": "sha512-EYC5CL7f+bSzrqdk1bylKqFNGabfiI5PDctxoPx70jFt89Jz+ThcOscENog8Jb4LEQFG6GkOYlwmPpsi7kx3QA==", + "dev": true, + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, + "node_modules/metro-runtime": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.80.10.tgz", + "integrity": "sha512-Xh0N589ZmSIgJYAM+oYwlzTXEHfASZac9TYPCNbvjNTn0EHKqpoJ/+Im5G3MZT4oZzYv4YnvzRtjqS5k0tK94A==", + "dev": true, + "peer": true, "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "@babel/runtime": "^7.0.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "node_modules/metro-source-map": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.80.10.tgz", + "integrity": "sha512-EyZswqJW8Uukv/HcQr6K19vkMXW1nzHAZPWJSEyJFKIbgp708QfRZ6vnZGmrtFxeJEaFdNup4bGnu8/mIOYlyA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.20.0", + "@babel/types": "^7.20.0", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.80.10", + "nullthrows": "^1.1.1", + "ob1": "0.80.10", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18" + } }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "node_modules/metro-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.80.10.tgz", + "integrity": "sha512-qAoVUoSxpfZ2DwZV7IdnQGXCSsf2cAUExUcZyuCqGlY5kaWBb0mx2BL/xbMFDJ4wBp3sVvSBPtK/rt4J7a0xBA==", + "dev": true, + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.80.10", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "through2": "^2.0.1", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18" + } }, - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" + "node_modules/metro-symbolicate/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.80.10.tgz", + "integrity": "sha512-leAx9gtA+2MHLsCeWK6XTLBbv2fBnNFu/QiYhWzMq8HsOAP4u1xQAU0tSgPs8+1vYO34Plyn79xTLUtQCRSSUQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.20.0", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + } }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", - "requires": { - "asap": "^2.0.0", - "wrappy": "1" + "node_modules/metro-transform-worker": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.80.10.tgz", + "integrity": "sha512-zNfNLD8Rz99U+JdOTqtF2o7iTjcDMMYdVS90z6+81Tzd2D0lDWVpls7R1hadS6xwM+ymgXFQTjM6V6wFoZaC0g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.0", + "@babel/parser": "^7.20.0", + "@babel/types": "^7.20.0", + "flow-enums-runtime": "^0.0.6", + "metro": "0.80.10", + "metro-babel-transformer": "0.80.10", + "metro-cache": "0.80.10", + "metro-cache-key": "0.80.10", + "metro-minify-terser": "0.80.10", + "metro-source-map": "0.80.10", + "metro-transform-plugins": "0.80.10", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" } }, - "dir-glob": { + "node_modules/metro/node_modules/ci-info": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "peer": true + }, + "node_modules/metro/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" } }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "requires": { - "is-obj": "^1.0.0" + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.0.tgz", + "integrity": "sha512-Rkp0PNLGpORw4ktsttkVbpYJbrYKS3hAnkxu8D9nvQi6LvSbuPa+tYw/t2u3Gjc35lYd/k95YkjqyTcN4zspag==", + "dev": true, + "peer": true + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.0.tgz", + "integrity": "sha512-xLwM4ylfHGwrm+2qXfO1JT/fnqEDGSnpS/9hQ4VLtqTexSviu2ZpBgz07U8jVtndq67qdb/ps0qvaWDZ3fkTyg==", + "dev": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.23.0" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + "node_modules/metro/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true }, - "duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" + "node_modules/metro/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" } }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" } }, - "err-code": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", - "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" } }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" } }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" } }, - "extsprintf": { + "node_modules/mocha-lcov-reporter": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz", + "integrity": "sha512-/5zI2tW4lq/ft8MGpYQ1nIH6yePPtIzdGeUEwFMKfMRdLfAQ1QW2c68eEJop32tNdN5srHa/E2TzB+erm3YMYA==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.2.tgz", - "integrity": "sha512-TR6zxCKftDQnUAPvkrCWdBgDq/gbqx8A3ApnBrR5rMvpp6+KMJI0Igw7fkWPgeVK0uhRXTXdvO3O+YP0CaUX2g==", - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.0.1", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.1", - "micromatch": "^3.1.10" - }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, "dependencies": { - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "requires": { - "is-extglob": "^2.1.1" - } - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" } }, - "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "requires": { - "minipass": "^2.2.1" + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "genfun": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/genfun/-/genfun-4.0.1.tgz", - "integrity": "sha1-7RAEHy5KfxsKOEZtF6XD4n3x38E=" + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + "node_modules/murmurhash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-2.0.1.tgz", + "integrity": "sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==" }, - "get-pkg-repo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", - "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", - "requires": { - "hosted-git-info": "^2.1.4", - "meow": "^3.3.0", - "normalize-package-data": "^2.3.0", - "parse-github-repo-url": "^1.3.0", - "through2": "^2.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "requires": { - "repeating": "^2.0.0" - } - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "requires": { - "get-stdin": "^4.0.1" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" - } + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + "node_modules/native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", + "dev": true }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" } }, - "git-raw-commits": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", - "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", - "requires": { - "dargs": "^4.0.1", - "lodash.template": "^4.0.2", - "meow": "^4.0.0", - "split2": "^2.0.0", - "through2": "^2.0.0" + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "dev": true, + "dependencies": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" } }, - "git-remote-origin-url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", - "requires": { - "gitconfiglocal": "^1.0.0", - "pify": "^2.3.0" - }, + "node_modules/nocache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.4.tgz", + "integrity": "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/nock": { + "version": "11.9.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-11.9.1.tgz", + "integrity": "sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA==", + "dev": true, "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.13", + "mkdirp": "^0.5.0", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 8.0" } }, - "git-semver-tags": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.6.tgz", - "integrity": "sha512-2jHlJnln4D/ECk9FxGEBh3k44wgYdWjWDtMmJPaecjoRmxKo3Y1Lh8GMYuOPu04CHw86NTAODchYjC5pnpMQig==", - "requires": { - "meow": "^4.0.0", - "semver": "^5.5.0" + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "peer": true + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "dev": true, + "peer": true, + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" } }, - "gitconfiglocal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", - "requires": { - "ini": "^1.3.2" + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "peer": true }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "peer": true }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } }, - "globby": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz", - "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==", - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" - } - } + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6.13.0" } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "peer": true }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" } }, - "has-flag": { + "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "has-values": { + "node_modules/null-check": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha512-j8ZNHg19TyIQOWCGeeQJBuu6xZYIEurf8M1Qsfd8mFrGEfIZytbw18YjKWg+LcO25NowXGZXZpKAx+Ui3TFfDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" - }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "dev": true, + "peer": true + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" } }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" - }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", - "requires": { - "ms": "^2.0.0" + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" } }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "requires": { - "minimatch": "^3.0.4" + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "init-package-json": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz", - "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", - "requires": { - "glob": "^7.1.1", - "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", - "promzard": "^0.3.0", - "read": "~1.0.1", - "read-package-json": "1 || 2", - "semver": "2.x || 3.x || 4 || 5", - "validate-npm-package-license": "^3.0.1", - "validate-npm-package-name": "^3.0.0" + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "node_modules/ob1": { + "version": "0.80.10", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.80.10.tgz", + "integrity": "sha512-dJHyB0S6JkMorUSfSGcYGkkg9kmq3qDUu3ygZUKIfkr47XOPuG35r2Sk6tbwtHXbdKIXmcMvM8DF2CwgdyaHfQ==", + "dev": true, + "peer": true, + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18" + } }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "^1.0.0" + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-ci": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.0.tgz", - "integrity": "sha512-plgvKjQtalH2P3Gytb7L61Lmz95g2DlpzFiQyRSFew8WoJKxtKRzrZMeyRN2supblm3Psc8OQGy7Xjb6XG11jw==", - "requires": { - "ci-info": "^1.3.0" + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } + "wrappy": "1" } }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dev": true, + "peer": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "^1.0.0" + "node_modules/open/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "peer": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "is-glob": { + "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "is-number": { + "node_modules/p-map": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "is-text-path": { + "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", - "requires": { - "text-extensions": "^1.0.0" + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "optional": true + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" + } }, - "lcid": { + "node_modules/prettier-linter-helpers": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "^1.0.0" + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" } }, - "lerna": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.2.1.tgz", - "integrity": "sha512-nHa/TgRLOHlBm+NfeW62ffVO7hY7wJxnu6IJmZA3lrSmRlqrXZk2BPvnq0FSaCinVYjW0w0XeSNZdRKR//HAwQ==", - "requires": { - "@lerna/add": "^3.2.0", - "@lerna/bootstrap": "^3.2.0", - "@lerna/changed": "^3.2.0", - "@lerna/clean": "^3.1.3", - "@lerna/cli": "^3.2.0", - "@lerna/create": "^3.1.3", - "@lerna/diff": "^3.1.3", - "@lerna/exec": "^3.1.3", - "@lerna/import": "^3.1.3", - "@lerna/init": "^3.1.3", - "@lerna/link": "^3.1.4", - "@lerna/list": "^3.1.3", - "@lerna/publish": "^3.2.1", - "@lerna/run": "^3.1.3", - "@lerna/version": "^3.2.0", - "import-local": "^1.0.0", - "npmlog": "^4.1.2" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "peer": true }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } }, - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dev": true, + "peer": true, + "dependencies": { + "asap": "~2.0.6" } }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "requires": { - "lodash._reinterpolate": "~3.0.0" + "node_modules/promise-polyfill": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.0.tgz", + "integrity": "sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" } }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" } }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" } }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" } }, - "make-fetch-happen": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz", - "integrity": "sha512-7R5ivfy9ilRJ1EMKIOziwrns9fGeAD4bAha8EB7BIiBBLHm2KeTUGCrICFt2rbHfzheTLynv50GnNTK1zDTrcQ==", - "requires": { - "agentkeepalive": "^3.4.1", - "cacache": "^11.0.1", - "http-cache-semantics": "^3.8.1", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", - "lru-cache": "^4.1.2", - "mississippi": "^3.0.0", - "node-fetch-npm": "^2.0.2", - "promise-retry": "^1.1.1", - "socks-proxy-agent": "^4.0.0", - "ssri": "^6.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", - "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=" + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" + "node_modules/querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.4.x" } }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "^1.0.0" + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "~2.0.3" } }, - "meow": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", - "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", - "requires": { - "camelcase-keys": "^4.0.0", - "decamelize-keys": "^1.0.0", - "loud-rejection": "^1.0.0", - "minimist": "^1.1.3", - "minimist-options": "^3.0.1", - "normalize-package-data": "^2.3.4", - "read-pkg-up": "^3.0.0", - "redent": "^2.0.0", - "trim-newlines": "^2.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ] }, - "merge2": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.2.tgz", - "integrity": "sha512-bgM8twH86rWni21thii6WCMQMRMmwqqdW3sGWi9IipnVAszdLXRjwDwAnyrVXo6DuP3AjRMMttZKUB48QWIFGg==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" } }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "requires": { - "mime-db": "~1.36.0" + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" + "node_modules/react-devtools-core": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.1.tgz", + "integrity": "sha512-7FSb9meX0btdBQLwdFOwt6bGqvRPabmVMMslv8fgoSPqXyuGpgQe36kx8gR86XPw7aV1yVouTp6fyZ0EH+NfUw==", + "dev": true, + "peer": true, + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "minimist": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", - "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=" + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true }, - "minimist-options": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", - "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" - } - }, - "minipass": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", - "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "node_modules/react-native": { + "version": "0.75.2", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.2.tgz", + "integrity": "sha512-pP+Yswd/EurzAlKizytRrid9LJaPJzuNldc+o5t01md2VLHym8V7FWH2z9omFKtFTer8ERg0fAhG1fpd0Qq6bQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.6.3", + "@react-native-community/cli": "14.0.0", + "@react-native-community/cli-platform-android": "14.0.0", + "@react-native-community/cli-platform-ios": "14.0.0", + "@react-native/assets-registry": "0.75.2", + "@react-native/codegen": "0.75.2", + "@react-native/community-cli-plugin": "0.75.2", + "@react-native/gradle-plugin": "0.75.2", + "@react-native/js-polyfills": "0.75.2", + "@react-native/normalize-colors": "0.75.2", + "@react-native/virtualized-lists": "0.75.2", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.6.3", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.80.3", + "metro-source-map": "^0.80.3", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^26.5.2", + "promise": "^8.3.0", + "react-devtools-core": "^5.3.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.2", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true } } }, - "minizlib": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "requires": { - "minipass": "^2.2.1" + "node_modules/react-native/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" } }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } + "node_modules/react-native/node_modules/@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" + "node_modules/react-native/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/react-native/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, + "node_modules/react-native/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "peer": true, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } + "async-limiter": "~1.0.0" } }, - "modify-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", - "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==" - }, - "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "node_modules/readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", + "dev": true, + "peer": true + }, + "node_modules/recast": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.5.tgz", + "integrity": "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==", + "dev": true, + "peer": true, + "dependencies": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "peer": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "peer": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "peer": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.8.4" } }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } }, - "node-fetch-npm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", - "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", - "requires": { - "encoding": "^0.1.11", - "json-parse-better-errors": "^1.0.0", - "safe-buffer": "^5.1.1" + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "peer": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" } }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" - } + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" } }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" } }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, - "npm-lifecycle": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-2.1.0.tgz", - "integrity": "sha512-QbBfLlGBKsktwBZLj6AviHC6Q9Y3R/AY4a2PYSIRhSKSS0/CxRyD/PfxEX6tPeOCXQgMSNdwGeECacstgptc+g==", - "requires": { - "byline": "^5.0.0", - "graceful-fs": "^4.1.11", - "node-gyp": "^3.8.0", - "resolve-from": "^4.0.0", - "slide": "^1.1.6", - "uid-number": "0.0.6", - "umask": "^1.1.0", - "which": "^1.3.1" + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "npm-package-arg": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz", - "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", - "requires": { - "hosted-git-info": "^2.6.0", - "osenv": "^0.1.5", - "semver": "^5.5.0", - "validate-npm-package-name": "^3.0.0" + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" } }, - "npm-packlist": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", - "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "peer": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" } }, - "npm-pick-manifest": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-2.1.0.tgz", - "integrity": "sha512-q9zLP8cTr8xKPmMZN3naxp1k/NxVFsjxN6uWuO1tiw9gxg7wZWQ/b5UTfzD0ANw2q1lQxdLKTeCCksq+bPSgbQ==", - "requires": { - "npm-package-arg": "^6.0.0", - "semver": "^5.4.1" + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "npm-registry-fetch": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.8.0.tgz", - "integrity": "sha512-hrw8UMD+Nob3Kl3h8Z/YjmKamb1gf7D1ZZch2otrIXM3uFLB5vjEY6DhMlq80z/zZet6eETLbOXcuQudCB3Zpw==", - "requires": { - "JSONStream": "^1.3.4", - "bluebird": "^3.5.1", - "figgy-pudding": "^3.4.1", - "lru-cache": "^4.1.3", - "make-fetch-happen": "^4.0.1", - "npm-package-arg": "^6.1.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "node_modules/rollup-plugin-terser": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.1.tgz", + "integrity": "sha512-1pkwkervMJQGFYvM9nscrUoncPwiKR/K+bHdjv6PFgRo3cgPHoRT83y2Aa3GvINj4539S15t/tpFPb775TDs6w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.5.5", + "jest-worker": "^24.9.0", + "rollup-pluginutils": "^2.8.2", + "serialize-javascript": "^4.0.0", + "terser": "^4.6.2" + }, + "peerDependencies": { + "rollup": ">=0.66.0 <3" + } }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "node_modules/rollup-plugin-terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "node_modules/rollup-plugin-terser/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" } }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" + "node_modules/rollup-plugin-terser/node_modules/jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "dependencies": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">= 6" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" + "node_modules/rollup-plugin-terser/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" + "node_modules/rollup-plugin-terser/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" } }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" + "node_modules/rollup-plugin-typescript2": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz", + "integrity": "sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "8.1.0", + "resolve": "1.17.0", + "tslib": "2.0.1" + }, + "peerDependencies": { + "rollup": ">=1.26.3", + "typescript": ">=2.4.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "node_modules/rollup-plugin-typescript2/node_modules/resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.6" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup-plugin-typescript2/node_modules/tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - } + "estree-walker": "^0.6.1" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, - "p-limit": { + "node_modules/samsam": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "deprecated": "This package has been deprecated in favour of @sinonjs/samsam", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" } }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" - }, - "p-map-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", - "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", - "requires": { - "p-reduce": "^1.0.0" + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" } }, - "p-pipe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", - "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=" - }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "p-waterfall": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", - "integrity": "sha1-ftlLPOszMngjU69qrhGqn8I1uwA=", - "requires": { - "p-reduce": "^1.0.0" - } - }, - "pacote": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.1.0.tgz", - "integrity": "sha512-AFXaSWhOtQf3jHqEvg+ZYH/dfT8TKq6TKspJ4qEFwVVuh5aGvMIk6SNF8vqfzz+cBceDIs9drOcpBbrPai7i+g==", - "requires": { - "bluebird": "^3.5.1", - "cacache": "^11.0.2", - "figgy-pudding": "^3.2.1", - "get-stream": "^3.0.0", - "glob": "^7.1.2", - "lru-cache": "^4.1.3", - "make-fetch-happen": "^4.0.1", - "minimatch": "^3.0.4", - "minipass": "^2.3.3", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "normalize-package-data": "^2.4.0", - "npm-package-arg": "^6.1.0", - "npm-packlist": "^1.1.10", - "npm-pick-manifest": "^2.1.0", - "npm-registry-fetch": "^3.0.0", - "osenv": "^0.1.5", - "promise-inflight": "^1.0.1", - "promise-retry": "^1.1.1", - "protoduck": "^5.0.0", - "rimraf": "^2.6.2", - "safe-buffer": "^5.1.2", - "semver": "^5.5.0", - "ssri": "^6.0.0", - "tar": "^4.4.3", - "unique-filename": "^1.1.0", - "which": "^1.3.0" - }, - "dependencies": { - "tar": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", - "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" - } + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "parse-github-repo-url": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", - "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=" - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" } }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "peer": true }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" } }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "^2.1.0" + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "peer": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "process-nextick-args": { + "node_modules/set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true }, - "promise-retry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", - "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", - "requires": { - "err-code": "^1.0.0", - "retry": "^0.10.0" - } + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, - "promzard": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", - "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", - "requires": { - "read": "1" + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "peer": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" } }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "protoduck": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.0.tgz", - "integrity": "sha512-agsGWD8/RZrS4ga6v82Fxb0RHIS2RZnbsSue6A9/MBRhB/jcqOANAMNrqM9900b8duj+Gx+T/JMy5IowDoO/hQ==", - "requires": { - "genfun": "^4.0.1" + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" - }, - "pump": { + "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" } }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "quick-lru": { + "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", - "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "requires": { - "mute-stream": "~0.0.4" + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "read-cmd-shim": { + "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz", - "integrity": "sha1-LV0Vd4ajfAVdIgd8MsU/gynpHHs=", - "requires": { - "graceful-fs": "^4.1.2" + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "read-package-json": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.13.tgz", - "integrity": "sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg==", - "requires": { - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "json-parse-better-errors": "^1.0.1", - "normalize-package-data": "^2.0.0", - "slash": "^1.0.0" + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "read-package-tree": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.2.1.tgz", - "integrity": "sha512-2CNoRoh95LxY47LvqrehIAfUVda2JbuFE/HaGYs42bNrGG+ojbw1h3zOcPcQ+1GQ3+rkzNndZn85u1XyZ3UsIA==", - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "once": "^1.3.0", - "read-package-json": "^2.0.0", - "readdir-scoped-modules": "^1.0.0" + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sinon": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", + "integrity": "sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==", + "deprecated": "16.1.1", + "dev": true, + "dependencies": { + "diff": "^3.1.0", + "formatio": "1.2.0", + "lolex": "^1.6.0", + "native-promise-only": "^0.8.1", + "path-to-regexp": "^1.7.0", + "samsam": "^1.1.3", + "text-encoding": "0.6.4", + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.1.103" } }, - "read-pkg": { + "node_modules/sinon/node_modules/lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha512-/bpxDL56TG5LS5zoXxKqA6Ro5tkOS5M8cm/7yQcwLIKIcM2HR5fjjNCaIhJNv96SEk4hNGSafYMZK42Xv5fihQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "peer": true + }, + "node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" } }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - } + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "readdir-scoped-modules": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz", - "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=", - "requires": { - "debuglog": "^1.0.1", - "dezalgo": "^1.0.0", - "graceful-fs": "^4.1.2", - "once": "^1.3.0" + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" } }, - "redent": { + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "peer": true + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", - "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", - "requires": { - "indent-string": "^3.0.0", - "strip-indent": "^2.0.0" + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/socket.io": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" } }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true }, - "resolve-cwd": { + "node_modules/spawn-wrap": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "requires": { - "resolve-from": "^3.0.0" - }, + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - } + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" } }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "node_modules/spawn-wrap/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, - "retry": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", - "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "optional": true, - "requires": { - "align-text": "^0.1.1" + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "peer": true + }, + "node_modules/stacktrace-parser": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", + "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", + "dev": true, + "peer": true, + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" } }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "^7.0.5" + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" } }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" } }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "requires": { - "aproba": "^1.1.1" + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" } }, - "rxjs": { - "version": "5.5.11", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", - "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", - "requires": { - "symbol-observable": "1.0.1" + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "set-blocking": { + "node_modules/strip-final-newline": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true, + "peer": true + }, + "node_modules/sudo-prompt": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", + "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", + "dev": true, + "peer": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "dev": true, + "peer": true, + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + "node_modules/temp-fs": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", + "integrity": "sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==", + "dev": true, + "dependencies": { + "rimraf": "~2.5.2" + }, + "engines": { + "node": ">=0.8.0" + } }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + "node_modules/temp-fs/node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } }, - "smart-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz", - "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.1.3" }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "dev": true, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" } }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } + "esbuild": { + "optional": true }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } + "uglify-js": { + "optional": true } } }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "socks": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.1.tgz", - "integrity": "sha512-0GabKw7n9mI46vcNrVfs0o6XzWzjVa3h6GaSo2UPxtWAROXUWavfJWh1M4PR5tnE0dcnQXZIDFP4yrAysLze/w==", - "requires": { - "ip": "^1.1.5", - "smart-buffer": "^4.0.1" + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" } }, - "socks-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz", - "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==", - "requires": { - "agent-base": "~4.2.0", - "socks": "~2.2.0" + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "requires": { - "is-plain-obj": "^1.0.0" - } + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "node_modules/text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==", + "deprecated": "no longer maintained", + "dev": true }, - "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true, + "peer": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "peer": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "requires": { - "through": "2" + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" } }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" } }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "requires": { - "through2": "^2.0.2" + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "requires": { - "figgy-pudding": "^3.5.1" + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" } }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ts-node/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "strip-bom": { + "node_modules/tsconfig-paths/node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" - }, - "strong-log-transformer": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-1.0.6.tgz", - "integrity": "sha1-9/uTdYpppXEUAYEnfuoMLrEwH6M=", - "requires": { - "byline": "^5.0.0", - "duplexer": "^0.1.1", - "minimist": "^0.1.0", - "moment": "^2.6.0", - "through": "^2.3.4" + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, + "peer": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" - }, - "temp-write": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz", - "integrity": "sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=", - "requires": { - "graceful-fs": "^4.1.2", - "is-stream": "^1.1.0", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "temp-dir": "^1.0.0", - "uuid": "^3.0.1" - } - }, - "text-extensions": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", - "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==" + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "requires": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" } }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" } }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "peer": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" } }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" } }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" } }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } + "punycode": "^2.1.0" } }, - "trim-newlines": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", - "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "peer": true }, - "trim-off-newlines": { + "node_modules/utils-merge": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", - "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typescript": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3.tgz", - "integrity": "sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A==" - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "umask": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", - "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true } } }, - "unique-filename": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", - "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", - "requires": { - "unique-slug": "^2.0.0" + "node_modules/vitest/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" } }, - "unique-slug": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", - "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", - "requires": { - "imurmurhash": "^0.1.4" + "node_modules/vitest/node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "node_modules/vitest/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "node_modules/vitest/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + "node_modules/vitest/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "node_modules/vitest/node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "node_modules/vitest/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" } }, - "validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", - "requires": { - "builtins": "^1.0.3" + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "dev": true, + "peer": true + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" } }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" } }, - "wcwidth": { + "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", - "requires": { + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "peer": true, + "dependencies": { "defaults": "^1.0.3" } }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" + "node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { + "node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true, + "peer": true + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "optional": true + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.4" } }, - "write-json-file": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", - "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", - "requires": { - "detect-indent": "^5.0.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "pify": "^3.0.0", - "sort-keys": "^2.0.0", - "write-file-atomic": "^2.0.0" - } - }, - "write-pkg": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz", - "integrity": "sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==", - "requires": { - "sort-keys": "^2.0.0", - "write-json-file": "^2.2.0" - } - }, - "xregexp": { + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "peer": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + } }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "requires": { - "camelcase": "^4.1.0" + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package.json b/package.json index 28bd3656f..675bb7d4f 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,178 @@ { - "private": true, - "version": "1.0.0", - "name": "optimizely-sdk-packages", + "name": "@optimizely/optimizely-sdk", + "version": "6.1.0", + "description": "JavaScript SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts", + "main": "./dist/index.node.min.js", + "browser": "./dist/index.browser.es.min.js", + "react-native": "./dist/index.react_native.min.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": { + "import": "./dist/index.node.es.min.mjs", + "require": "./dist/index.node.min.js" + }, + "react-native": { + "import": "./dist/index.react_native.es.min.js", + "require": "./dist/index.react_native.min.js" + }, + "browser": { + "import": "./dist/index.browser.es.min.js", + "require": "./dist/index.browser.min.js" + }, + "default": { + "import": "./dist/index.node.es.min.mjs", + "require": "./dist/index.node.min.js" + } + }, + "./node": { + "types": "./dist/index.d.ts", + "import": "./dist/index.node.es.min.mjs", + "require": "./dist/index.node.min.js" + }, + "./browser": { + "types": "./dist/index.d.ts", + "import": "./dist/index.browser.es.min.js", + "require": "./dist/index.browser.min.js" + }, + "./react_native": { + "types": "./dist/index.d.ts", + "default": "./dist/index.react_native.min.js", + "import": "./dist/index.react_native.es.min.js", + "require": "./dist/index.react_native.min.js" + }, + "./universal": { + "types": "./dist/index.universal.d.ts", + "import": "./dist/index.universal.es.min.js", + "require": "./dist/index.universal.min.js" + }, + "./ua_parser": { + "types": "./dist/odp/ua_parser/ua_parser.d.ts", + "import": "./dist/ua_parser.es.min.js", + "require": "./dist/ua_parser.min.js" + } + }, "scripts": { - "build": "lerna run build", - "clean": "lerna run clean", - "publish": "npm run build && lerna publish", - "test": "lerna run test --stream", - "test-xbrowser": "lerna run test-xbrowser --stream" + "clean": "rm -rf dist", + "clean:win": "(if exist dist rd /s/q dist)", + "lint": "tsc --noEmit && eslint 'lib/**/*.js' 'lib/**/*.ts'", + "test-vitest": "vitest run", + "test-mocha": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register -r tsconfig-paths/register -r lib/tests/exit_on_unhandled_rejection.js 'lib/**/*.tests.ts' 'lib/**/*.tests.js'", + "test": "npm run test-mocha && npm run test-vitest", + "posttest": "npm run lint", + "test-ci": "npm run test-xbrowser && npm run test-umdbrowser", + "test-xbrowser": "karma start karma.bs.conf.js --single-run", + "test-umdbrowser": "npm run build-browser-umd && karma start karma.umd.conf.js --single-run", + "test-karma-local": "karma start karma.local_chrome.bs.conf.js && npm run build-browser-umd && karma start karma.local_chrome.umd.conf.js", + "prebuild": "npm run clean", + "build": "npm run genmsg && rollup -c && cp dist/index.browser.d.ts dist/index.d.ts", + "build:win": "npm run genmsg && rollup -c && type nul > dist/optimizely.lite.es.d.ts && type nul > dist/optimizely.lite.es.min.d.ts && type nul > dist/optimizely.lite.min.d.ts", + "build-browser-umd": "rollup -c --config-umd", + "coveralls": "nyc --reporter=lcov npm test", + "prepare": "npm run build", + "prepublishOnly": "npm test", + "postbuild:win": "@powershell copy \"dist/index.lite.d.ts\" \"dist/optimizely.lite.es.d.ts\" && @powershell copy \"dist/index.lite.d.ts\" \"dist/optimizely.lite.es.min.d.ts\" && @powershell copy \"dist/index.lite.d.ts\" \"dist/optimizely.lite.min.d.ts\"", + "genmsg": "jiti message_generator ./lib/message/error_message.ts ./lib/message/log_message.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/optimizely/javascript-sdk.git" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" }, + "keywords": [ + "optimizely" + ], + "bugs": { + "url": "https://github.com/optimizely/javascript-sdk/issues" + }, + "homepage": "https://github.com/optimizely/javascript-sdk", "dependencies": { - "lerna": "^3.2.1", - "typescript": "^3.3.3" + "decompress-response": "^7.0.0", + "json-schema": "^0.4.0", + "murmurhash": "^2.0.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@react-native-async-storage/async-storage": "^2", + "@react-native-community/netinfo": "^11.3.2", + "@rollup/plugin-commonjs": "^11.0.2", + "@rollup/plugin-node-resolve": "^7.1.1", + "@types/chai": "^4.2.11", + "@types/mocha": "^5.2.7", + "@types/nise": "^1.4.0", + "@types/node": "^18.7.18", + "@types/ua-parser-js": "^0.7.36", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^5.33.0", + "@typescript-eslint/parser": "^5.33.0", + "@vitest/coverage-istanbul": "^2.0.5", + "chai": "^4.2.0", + "coveralls-next": "^4.2.0", + "eslint": "^8.21.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-prettier": "^3.1.2", + "happy-dom": "^16.6.0", + "jiti": "^2.4.1", + "karma": "^6.4.0", + "karma-browserstack-launcher": "^1.5.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.1.1", + "karma-mocha": "^2.0.1", + "karma-webpack": "^5.0.1", + "lodash": "^4.17.11", + "mocha": "^10.2.0", + "mocha-lcov-reporter": "^1.3.0", + "nise": "^1.4.10", + "nock": "11.9.1", + "nyc": "^15.0.1", + "prettier": "^1.19.1", + "promise-polyfill": "8.1.0", + "rollup": "2.79.2", + "rollup-plugin-terser": "^5.3.0", + "rollup-plugin-typescript2": "^0.27.1", + "sinon": "^2.3.1", + "ts-loader": "^9.3.1", + "ts-node": "^8.10.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^4.7.4", + "vitest": "^2.0.5", + "webpack": "^5.74.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": ">=1.0.0 <3.0.0", + "@react-native-community/netinfo": ">=5.0.0 <12.0.0", + "fast-text-encoding": "^1.0.6", + "react-native-get-random-values": "^1.11.0", + "ua-parser-js": "^1.0.38" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + }, + "@react-native-community/netinfo": { + "optional": true + }, + "react-native-get-random-values": { + "optional": true + }, + "fast-text-encoding": { + "optional": true + }, + "ua-parser-js": { + "optional": true + } + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/" + ], + "nyc": { + "temp-dir": "coverage/raw" } } diff --git a/packages/logging/.gitignore b/packages/logging/.gitignore deleted file mode 100644 index d4a125901..000000000 --- a/packages/logging/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib/ -doc/ diff --git a/packages/logging/CHANGELOG.MD b/packages/logging/CHANGELOG.MD deleted file mode 100644 index dbf1b2562..000000000 --- a/packages/logging/CHANGELOG.MD +++ /dev/null @@ -1,12 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## [Unreleased] -Changes that have landed but are not yet released. - -## [0.1.0] - March 1, 2019 - -Initial release \ No newline at end of file diff --git a/packages/logging/LICENSE b/packages/logging/LICENSE deleted file mode 100644 index b9f80c5bd..000000000 --- a/packages/logging/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2016-2017, Optimizely, Inc. and contributors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/logging/README.md b/packages/logging/README.md deleted file mode 100644 index efa69a82d..000000000 --- a/packages/logging/README.md +++ /dev/null @@ -1,156 +0,0 @@ -# Javascript SDK Logging - -Provides a centralized LogManager and errorHandler for Javascript SDK packages. - -## Installation - -```sh -npm install @optimizely/js-sdk-logging -``` - -## Architecture - -![Logging Architecture](./logging_architecture.png) - - - - **LogHandler** - the component that determines where to write logs. Common log handlers - are `ConsoleLogHandler` or `NoopLogHandler` - - **LogManager** - returns Logger facade instances via LogManager.getLogger(name) - - **LoggerFacade** the internal logging interface available to other packages via `LogManager.getLogger(name)` - - -## Usage - - -#### Using the logger - -```typescript -import { getLogger } from '@optimizely/js-sdk-logging' - -const logger = getLogger('myModule') -logger.log('warn', 'this is a warning') - -logger.debug('string interpolation is easy and %s', 'lazily evaluated') - -logger.info('info logging') -logger.warn('this is a warning') -logger.error('this is an error') - -// `info` `warn` `debug` and `error` all support passing an Error as the last argument -// this will call the registered errorHandler -logger.error('an error occurred: %s', ex.message) - -// also Error passes to errorHandler.handleError(ex) -logger.error('an error occurred: %s', ex.message, ex) - -// if no message is passed will log `ex.message` -logger.error(ex) -``` - -#### Setting the log level - -```typescript -import { LogLevel, setLogLevel } from '@optimizely/js-sdk-logging' - -// can use enum -setLogLevel(LogLevel.ERROR) - -// can also use a string (lowercase or uppercase) -setLogLevel('debug') -setLogLevel('info') -setLogLevel('warn') -setLogLevel('error') -``` - - -#### Setting a LogHandler - -```typescript -import { setLogHandler, ConsoleLogHandler } from '@optimizely/js-sdk-logging' - -const handler = new ConsoleLogHandler({ - logLevel: 'error', - prefix: '[My custom prefix]', // defaults to "[OPTIMIZELY]" -}) - -setLogHandler(handler) -``` - -#### Implementing a custom LogHandler - -Perhaps you want to integrate Optimizely with your own logging system or use an existing library. - -A valid `LogHandler` is anything that implements this interface - -```typescript -interface LogHandler { - log(level: LogLevel, message: string): void -} -``` - -**Example: integrating with Winston** - -```js -import winston from 'winston' -import { setLogHandler, LogLevel } from '@optimizely/js-sdk-logging' - -const winstonLogger = winston.createLogger({ - level: 'info', - format: winston.format.json(), - defaultMeta: { service: 'optimizely' }, - transports: [ - new winston.transports.File({ filename: 'combined.log' }), - ], -}) - -/** - * Convert from optimizely log levels to winston - */ -function convertLogLevels(level) { - switch(level) { - case LogLevel.DEBUG: - return 'debug' - case LogLevel.INFO: - return 'info' - case LogLevel.WARNING: - return 'warning' - case LogLevel.ERROR: - return 'error' - default: - return 'silly' - } -} - -setLogHandler({ - log(level, message) { - winstoLogger.log({ - level: convertLogLevels(level), - message, - }) - } -}) -``` - -### API Interfaces - -```typescript -interface LoggerFacade { - log(level: LogLevel | string, message: string): void - - info(message: string | Error, ...splat: any[]): void - - debug(message: string | Error, ...splat: any[]): void - - warn(message: string | Error, ...splat: any[]): void - - error(message: string | Error, ...splat: any[]): void -} - -interface LogManager { - getLogger(name?: string): LoggerFacade -} - -interface LogHandler { - log(level: LogLevel, message: string): void -} -``` \ No newline at end of file diff --git a/packages/logging/__tests__/logger.spec.ts b/packages/logging/__tests__/logger.spec.ts deleted file mode 100644 index 10529d94a..000000000 --- a/packages/logging/__tests__/logger.spec.ts +++ /dev/null @@ -1,386 +0,0 @@ -/// <reference types="jest" /> -import { - LogLevel, - LogHandler, - LoggerFacade, -} from '../src/models' - -import { - setLogHandler, - setLogLevel, - getLogger, - ConsoleLogHandler, - resetLogger, - getLogLevel, -} from '../src/logger' - -import { resetErrorHandler } from '../src/errorHandler' -import { ErrorHandler, setErrorHandler } from '../src/errorHandler' - -describe('logger', () => { - afterEach(() => { - resetLogger() - resetErrorHandler() - }) - - describe('OptimizelyLogger', () => { - let stubLogger: LogHandler - let logger: LoggerFacade - let stubErrorHandler: ErrorHandler - - beforeEach(() => { - stubLogger = { - log: jest.fn(), - } - stubErrorHandler = { - handleError: jest.fn(), - } - setLogLevel(LogLevel.DEBUG) - setLogHandler(stubLogger) - setErrorHandler(stubErrorHandler) - logger = getLogger() - }) - - describe('setLogLevel', () => { - it('should coerce "debug"', () => { - setLogLevel('debug') - expect(getLogLevel()).toBe(LogLevel.DEBUG) - }) - - it('should coerce "deBug"', () => { - setLogLevel('deBug') - expect(getLogLevel()).toBe(LogLevel.DEBUG) - }) - - it('should coerce "INFO"', () => { - setLogLevel('INFO') - expect(getLogLevel()).toBe(LogLevel.INFO) - }) - - it('should coerce "WARN"', () => { - setLogLevel('WARN') - expect(getLogLevel()).toBe(LogLevel.WARNING) - }) - - it('should coerce "warning"', () => { - setLogLevel('warning') - expect(getLogLevel()).toBe(LogLevel.WARNING) - }) - - it('should coerce "ERROR"', () => { - setLogLevel('WARN') - expect(getLogLevel()).toBe(LogLevel.WARNING) - }) - - it('should default to error if invalid', () => { - setLogLevel('invalid') - expect(getLogLevel()).toBe(LogLevel.ERROR) - }) - }) - - describe('getLogger(name)', () => { - it('should prepend the name in the log messages', () => { - const myLogger = getLogger('doit') - myLogger.info('test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'doit: test') - }) - }) - - describe('logger.log(level, msg)', () => { - it('should work with a string logLevel', () => { - setLogLevel(LogLevel.INFO) - logger.log('info', 'test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test') - }) - - it('should call the loggerBackend when the message logLevel is equal to the configured logLevel threshold', () => { - setLogLevel(LogLevel.INFO) - logger.log(LogLevel.INFO, 'test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test') - }) - - it('should call the loggerBackend when the message logLevel is above to the configured logLevel threshold', () => { - setLogLevel(LogLevel.INFO) - logger.log(LogLevel.WARNING, 'test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test') - }) - - it('should not call the loggerBackend when the message logLevel is above to the configured logLevel threshold', () => { - setLogLevel(LogLevel.INFO) - logger.log(LogLevel.DEBUG, 'test') - - expect(stubLogger.log).toHaveBeenCalledTimes(0) - }) - - it('should not throw if loggerBackend is not supplied', () => { - setLogLevel(LogLevel.INFO) - logger.log(LogLevel.ERROR, 'test') - }) - }) - - describe('logger.info', () => { - it('should handle info(message)', () => { - logger.info('test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test') - }) - it('should handle info(message, ...splat)', () => { - logger.info('test: %s %s', 'hey', 'jude') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test: hey jude') - }) - - it('should handle info(message, ...splat, error)', () => { - const error = new Error('hey') - logger.info('test: %s', 'hey', error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'test: hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - - it('should handle info(error)', () => { - const error = new Error('hey') - logger.info(error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.INFO, 'hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - }) - - describe('logger.debug', () => { - it('should handle debug(message)', () => { - logger.debug('test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test') - }) - - it('should handle debug(message, ...splat)', () => { - logger.debug('test: %s', 'hey') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test: hey') - }) - - it('should handle debug(message, ...splat, error)', () => { - const error = new Error('hey') - logger.debug('test: %s', 'hey', error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'test: hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - - it('should handle debug(error)', () => { - const error = new Error('hey') - logger.debug(error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.DEBUG, 'hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - }) - - describe('logger.warn', () => { - it('should handle warn(message)', () => { - logger.warn('test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test') - }) - - it('should handle warn(message, ...splat)', () => { - logger.warn('test: %s', 'hey') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test: hey') - }) - - it('should handle warn(message, ...splat, error)', () => { - const error = new Error('hey') - logger.warn('test: %s', 'hey', error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'test: hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - - it('should handle info(error)', () => { - const error = new Error('hey') - logger.warn(error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.WARNING, 'hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - }) - - describe('logger.error', () => { - it('should handle error(message)', () => { - logger.error('test') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test') - }) - - it('should handle error(message, ...splat)', () => { - logger.error('test: %s', 'hey') - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test: hey') - }) - - it('should handle error(message, ...splat, error)', () => { - const error = new Error('hey') - logger.error('test: %s', 'hey', error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'test: hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - - it('should handle error(error)', () => { - const error = new Error('hey') - logger.error(error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'hey') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - - it('should work with an insufficient amount of splat args error(msg, ...splat, message)', () => { - const error = new Error('hey') - logger.error('hey %s', error) - - expect(stubLogger.log).toHaveBeenCalledTimes(1) - expect(stubLogger.log).toHaveBeenCalledWith(LogLevel.ERROR, 'hey undefined') - expect(stubErrorHandler.handleError).toHaveBeenCalledWith(error) - }) - }) - - describe('using ConsoleLoggerHandler', () => { - beforeEach(() => { - jest.spyOn(console, 'info').mockImplementation(() => {}) - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - it('should work with BasicLogger', () => { - const logger = new ConsoleLogHandler() - const TIME = '12:00' - setLogHandler(logger) - setLogLevel(LogLevel.INFO) - jest.spyOn(logger, 'getTime').mockImplementation(() => TIME) - - logger.log(LogLevel.INFO, 'hey') - - expect(console.info).toBeCalledTimes(1) - expect(console.info).toBeCalledWith('[OPTIMIZELY] - INFO 12:00 hey') - }) - - it('should set logLevel to ERROR when setLogLevel is called with invalid value', () => { - const logger = new ConsoleLogHandler() - logger.setLogLevel('invalid' as any) - - expect(logger.logLevel).toEqual(LogLevel.ERROR) - }) - - it('should set logLevel to ERROR when setLogLevel is called with no value', () => { - const logger = new ConsoleLogHandler() - // @ts-ignore - logger.setLogLevel() - - expect(logger.logLevel).toEqual(LogLevel.ERROR) - }) - }) - }) - - describe('ConsoleLogger', function() { - beforeEach(() => { - jest.spyOn(console, 'info') - jest.spyOn(console, 'log') - jest.spyOn(console, 'warn') - jest.spyOn(console, 'error') - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - it('should log to console.info for LogLevel.INFO', () => { - const logger = new ConsoleLogHandler({ - logLevel: LogLevel.DEBUG, - }) - const TIME = '12:00' - jest.spyOn(logger, 'getTime').mockImplementation(() => TIME) - - logger.log(LogLevel.INFO, 'test') - - expect(console.info).toBeCalledTimes(1) - expect(console.info).toBeCalledWith('[OPTIMIZELY] - INFO 12:00 test') - }) - - it('should log to console.log for LogLevel.DEBUG', () => { - const logger = new ConsoleLogHandler({ - logLevel: LogLevel.DEBUG, - }) - const TIME = '12:00' - jest.spyOn(logger, 'getTime').mockImplementation(() => TIME) - - logger.log(LogLevel.DEBUG, 'debug') - - expect(console.log).toBeCalledTimes(1) - expect(console.log).toBeCalledWith('[OPTIMIZELY] - DEBUG 12:00 debug') - }) - - it('should log to console.warn for LogLevel.WARNING', () => { - const logger = new ConsoleLogHandler({ - logLevel: LogLevel.DEBUG, - }) - const TIME = '12:00' - jest.spyOn(logger, 'getTime').mockImplementation(() => TIME) - - logger.log(LogLevel.WARNING, 'warning') - - expect(console.warn).toBeCalledTimes(1) - expect(console.warn).toBeCalledWith('[OPTIMIZELY] - WARN 12:00 warning') - }) - - it('should log to console.error for LogLevel.ERROR', () => { - const logger = new ConsoleLogHandler({ - logLevel: LogLevel.DEBUG, - }) - const TIME = '12:00' - jest.spyOn(logger, 'getTime').mockImplementation(() => TIME) - - logger.log(LogLevel.ERROR, 'error') - - expect(console.error).toBeCalledTimes(1) - expect(console.error).toBeCalledWith('[OPTIMIZELY] - ERROR 12:00 error') - }) - - it('should not log if the configured logLevel is higher', () => { - const logger = new ConsoleLogHandler({ - logLevel: LogLevel.INFO, - }) - - logger.log(LogLevel.DEBUG, 'debug') - - expect(console.log).toBeCalledTimes(0) - }) - }) -}) diff --git a/packages/logging/jest.config.js b/packages/logging/jest.config.js deleted file mode 100644 index 391afe8eb..000000000 --- a/packages/logging/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - // "roots": [ - // "./src" - // ], - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ], -} \ No newline at end of file diff --git a/packages/logging/logging_architecture.png b/packages/logging/logging_architecture.png deleted file mode 100644 index 7f01f74b5..000000000 Binary files a/packages/logging/logging_architecture.png and /dev/null differ diff --git a/packages/logging/package-lock.json b/packages/logging/package-lock.json deleted file mode 100644 index 44dc2f675..000000000 --- a/packages/logging/package-lock.json +++ /dev/null @@ -1,5566 +0,0 @@ -{ - "name": "@optimizely/js-sdk-logging", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - } - } - }, - "@optimizely/js-sdk-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.1.0.tgz", - "integrity": "sha512-p7499GgVaX94YmkrwOiEtLgxgjXTPbUQsvETaAil5J7zg1TOA4Wl8ClalLSvCh+AKWkxGdkL4/uM/zfbxPSNNw==", - "requires": { - "uuid": "^3.3.2" - } - }, - "@types/jest": { - "version": "23.3.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.14.tgz", - "integrity": "sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug==", - "dev": true - }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", - "dev": true - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", - "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", - "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", - "dev": true - } - } - }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", - "dev": true - }, - "ajv": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", - "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", - "dev": true, - "requires": { - "default-require-extensions": "^1.0.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", - "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", - "dev": true, - "requires": { - "babel-plugin-istanbul": "^4.1.6", - "babel-preset-jest": "^23.2.0" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-istanbul": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", - "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.13.0", - "find-up": "^2.1.0", - "istanbul-lib-instrument": "^1.10.1", - "test-exclude": "^4.2.1" - } - }, - "babel-plugin-jest-hoist": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", - "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-preset-jest": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", - "integrity": "sha1-jsegOhOPABoaj7HoETZSvxpV2kY=", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^23.2.0", - "babel-plugin-syntax-object-rest-spread": "^6.13.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "capture-exit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", - "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", - "dev": true, - "requires": { - "rsvp": "^3.3.3" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true, - "optional": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", - "dev": true - }, - "cssstyle": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", - "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", - "dev": true, - "requires": { - "strip-bom": "^2.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "dev": true, - "requires": { - "merge": "^1.2.0" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } - }, - "expect": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", - "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "jest-diff": "^23.6.0", - "jest-get-type": "^22.1.0", - "jest-matcher-utils": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-regex-util": "^23.3.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "^2.0.0" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", - "dev": true, - "requires": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "dev": true, - "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", - "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", - "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", - "dev": true, - "requires": { - "async": "^2.1.4", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.1", - "istanbul-lib-hook": "^1.2.2", - "istanbul-lib-instrument": "^1.10.2", - "istanbul-lib-report": "^1.1.5", - "istanbul-lib-source-maps": "^1.2.6", - "istanbul-reports": "^1.5.1", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", - "once": "^1.4.0" - } - }, - "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", - "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", - "dev": true, - "requires": { - "append-transform": "^0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": true, - "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" - } - }, - "istanbul-lib-report": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", - "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", - "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", - "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", - "dev": true, - "requires": { - "handlebars": "^4.0.3" - } - }, - "jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", - "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", - "dev": true, - "requires": { - "import-local": "^1.0.0", - "jest-cli": "^23.6.0" - }, - "dependencies": { - "jest-cli": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", - "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "import-local": "^1.0.0", - "is-ci": "^1.0.10", - "istanbul-api": "^1.3.1", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-instrument": "^1.10.1", - "istanbul-lib-source-maps": "^1.2.4", - "jest-changed-files": "^23.4.2", - "jest-config": "^23.6.0", - "jest-environment-jsdom": "^23.4.0", - "jest-get-type": "^22.1.0", - "jest-haste-map": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-regex-util": "^23.3.0", - "jest-resolve-dependencies": "^23.6.0", - "jest-runner": "^23.6.0", - "jest-runtime": "^23.6.0", - "jest-snapshot": "^23.6.0", - "jest-util": "^23.4.0", - "jest-validate": "^23.6.0", - "jest-watcher": "^23.4.0", - "jest-worker": "^23.2.0", - "micromatch": "^2.3.11", - "node-notifier": "^5.2.1", - "prompts": "^0.1.9", - "realpath-native": "^1.0.0", - "rimraf": "^2.5.4", - "slash": "^1.0.0", - "string-length": "^2.0.0", - "strip-ansi": "^4.0.0", - "which": "^1.2.12", - "yargs": "^11.0.0" - } - } - } - }, - "jest-changed-files": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.4.2.tgz", - "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", - "dev": true, - "requires": { - "throat": "^4.0.0" - } - }, - "jest-config": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", - "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-jest": "^23.6.0", - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^23.4.0", - "jest-environment-node": "^23.4.0", - "jest-get-type": "^22.1.0", - "jest-jasmine2": "^23.6.0", - "jest-regex-util": "^23.3.0", - "jest-resolve": "^23.6.0", - "jest-util": "^23.4.0", - "jest-validate": "^23.6.0", - "micromatch": "^2.3.11", - "pretty-format": "^23.6.0" - } - }, - "jest-diff": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", - "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "diff": "^3.2.0", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" - } - }, - "jest-docblock": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-23.2.0.tgz", - "integrity": "sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - }, - "jest-each": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", - "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "pretty-format": "^23.6.0" - } - }, - "jest-environment-jsdom": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz", - "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", - "dev": true, - "requires": { - "jest-mock": "^23.2.0", - "jest-util": "^23.4.0", - "jsdom": "^11.5.1" - } - }, - "jest-environment-node": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.4.0.tgz", - "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", - "dev": true, - "requires": { - "jest-mock": "^23.2.0", - "jest-util": "^23.4.0" - } - }, - "jest-get-type": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", - "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", - "dev": true - }, - "jest-haste-map": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", - "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", - "dev": true, - "requires": { - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.1.11", - "invariant": "^2.2.4", - "jest-docblock": "^23.2.0", - "jest-serializer": "^23.0.1", - "jest-worker": "^23.2.0", - "micromatch": "^2.3.11", - "sane": "^2.0.0" - } - }, - "jest-jasmine2": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", - "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", - "dev": true, - "requires": { - "babel-traverse": "^6.0.0", - "chalk": "^2.0.1", - "co": "^4.6.0", - "expect": "^23.6.0", - "is-generator-fn": "^1.0.0", - "jest-diff": "^23.6.0", - "jest-each": "^23.6.0", - "jest-matcher-utils": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-snapshot": "^23.6.0", - "jest-util": "^23.4.0", - "pretty-format": "^23.6.0" - } - }, - "jest-leak-detector": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", - "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", - "dev": true, - "requires": { - "pretty-format": "^23.6.0" - } - }, - "jest-matcher-utils": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", - "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" - } - }, - "jest-message-util": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", - "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0-beta.35", - "chalk": "^2.0.1", - "micromatch": "^2.3.11", - "slash": "^1.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-23.2.0.tgz", - "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", - "dev": true - }, - "jest-regex-util": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", - "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", - "dev": true - }, - "jest-resolve": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", - "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", - "dev": true, - "requires": { - "browser-resolve": "^1.11.3", - "chalk": "^2.0.1", - "realpath-native": "^1.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", - "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", - "dev": true, - "requires": { - "jest-regex-util": "^23.3.0", - "jest-snapshot": "^23.6.0" - } - }, - "jest-runner": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", - "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "graceful-fs": "^4.1.11", - "jest-config": "^23.6.0", - "jest-docblock": "^23.2.0", - "jest-haste-map": "^23.6.0", - "jest-jasmine2": "^23.6.0", - "jest-leak-detector": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-runtime": "^23.6.0", - "jest-util": "^23.4.0", - "jest-worker": "^23.2.0", - "source-map-support": "^0.5.6", - "throat": "^4.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - } - } - }, - "jest-runtime": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", - "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-plugin-istanbul": "^4.1.6", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "exit": "^0.1.2", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.11", - "jest-config": "^23.6.0", - "jest-haste-map": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-regex-util": "^23.3.0", - "jest-resolve": "^23.6.0", - "jest-snapshot": "^23.6.0", - "jest-util": "^23.4.0", - "jest-validate": "^23.6.0", - "micromatch": "^2.3.11", - "realpath-native": "^1.0.0", - "slash": "^1.0.0", - "strip-bom": "3.0.0", - "write-file-atomic": "^2.1.0", - "yargs": "^11.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "jest-serializer": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-23.0.1.tgz", - "integrity": "sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU=", - "dev": true - }, - "jest-snapshot": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", - "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", - "dev": true, - "requires": { - "babel-types": "^6.0.0", - "chalk": "^2.0.1", - "jest-diff": "^23.6.0", - "jest-matcher-utils": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-resolve": "^23.6.0", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^23.6.0", - "semver": "^5.5.0" - } - }, - "jest-util": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", - "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", - "dev": true, - "requires": { - "callsites": "^2.0.0", - "chalk": "^2.0.1", - "graceful-fs": "^4.1.11", - "is-ci": "^1.0.10", - "jest-message-util": "^23.4.0", - "mkdirp": "^0.5.1", - "slash": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "jest-validate": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", - "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "leven": "^2.1.0", - "pretty-format": "^23.6.0" - } - }, - "jest-watcher": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.4.0.tgz", - "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "string-length": "^2.0.0" - } - }, - "jest-worker": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", - "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", - "dev": true, - "requires": { - "merge-stream": "^1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.1", - "escodegen": "^1.9.1", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", - "xml-name-validator": "^3.0.0" - } - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "kleur": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", - "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", - "dev": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", - "dev": true - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "dev": true - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", - "dev": true - }, - "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", - "dev": true, - "requires": { - "mime-db": "~1.38.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nan": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-notifier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz", - "integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", - "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true, - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "prompts": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", - "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", - "dev": true, - "requires": { - "kleur": "^2.0.1", - "sisteransi": "^0.1.1" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "realpath-native": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", - "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", - "dev": true, - "requires": { - "util.promisify": "^1.0.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "dev": true, - "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", - "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "capture-exit": "^1.2.0", - "exec-sh": "^0.2.0", - "fb-watchman": "^2.0.0", - "fsevents": "^1.2.3", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5", - "watch": "~0.18.0" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "sisteransi": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", - "integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "test-exclude": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", - "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "micromatch": "^2.3.11", - "object-assign": "^4.1.0", - "read-pkg-up": "^1.0.1", - "require-main-filename": "^1.0.1" - } - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "ts-jest": { - "version": "23.10.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.10.5.tgz", - "integrity": "sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A==", - "dev": true, - "requires": { - "bs-logger": "0.x", - "buffer-from": "1.x", - "fast-json-stable-stringify": "2.x", - "json5": "2.x", - "make-error": "1.x", - "mkdirp": "0.x", - "resolve": "1.x", - "semver": "^5.5", - "yargs-parser": "10.x" - }, - "dependencies": { - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "dev": true, - "requires": { - "exec-sh": "^0.2.0", - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", - "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" - } - }, - "yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } -} diff --git a/packages/logging/package.json b/packages/logging/package.json deleted file mode 100644 index 363e598d7..000000000 --- a/packages/logging/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@optimizely/js-sdk-logging", - "version": "0.1.0", - "description": "Optimizely Full Stack Core Logging", - "author": "jordangarcia <jordan@optimizely.com>", - "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/logging", - "license": "MIT", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "directories": { - "lib": "lib", - "test": "test" - }, - "files": [ - "lib", - "LICENSE", - "CHANGELOG", - "README.md", - "package.json" - ], - "scripts": { - "tsc": "rm -rf lib && tsc", - "test": "jest", - "prepublishOnly": "jest && npm run tsc" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/optimizely/javascript-sdk.git" - }, - "keywords": [ - "optimizely" - ], - "bugs": { - "url": "https://github.com/optimizely/javascript-sdk/issues" - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@optimizely/js-sdk-utils": "^0.1.0" - }, - "devDependencies": { - "@types/jest": "^23.3.12", - "jest": "^23.6.0", - "ts-jest": "^23.10.5" - } -} diff --git a/packages/logging/src/errorHandler.ts b/packages/logging/src/errorHandler.ts deleted file mode 100644 index bb659aeae..000000000 --- a/packages/logging/src/errorHandler.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright 2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * @export - * @interface ErrorHandler - */ -export interface ErrorHandler { - /** - * @param {Error} exception - * @memberof ErrorHandler - */ - handleError(exception: Error): void -} - -/** - * @export - * @class NoopErrorHandler - * @implements {ErrorHandler} - */ -export class NoopErrorHandler implements ErrorHandler { - /** - * @param {Error} exception - * @memberof NoopErrorHandler - */ - handleError(exception: Error): void { - // no-op - return - } -} - -let globalErrorHandler: ErrorHandler = new NoopErrorHandler() - -/** - * @export - * @param {ErrorHandler} handler - */ -export function setErrorHandler(handler: ErrorHandler): void { - globalErrorHandler = handler -} - -/** - * @export - * @returns {ErrorHandler} - */ -export function getErrorHandler(): ErrorHandler { - return globalErrorHandler -} - -/** - * @export - */ -export function resetErrorHandler(): void { - globalErrorHandler = new NoopErrorHandler() -} diff --git a/packages/logging/src/logger.ts b/packages/logging/src/logger.ts deleted file mode 100644 index 269968372..000000000 --- a/packages/logging/src/logger.ts +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Copyright 2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { getErrorHandler } from './errorHandler' -import { isValidEnum, sprintf } from '@optimizely/js-sdk-utils' - -import { LogLevel, LoggerFacade, LogManager, LogHandler } from './models' - -const stringToLogLevel = { - NOTSET: 0, - DEBUG: 1, - INFO: 2, - WARNING: 3, - ERROR: 4, -} - -function coerceLogLevel(level: any): LogLevel { - if (typeof level !== 'string') { - return level - } - - level = level.toUpperCase() - if (level === 'WARN') { - level = 'WARNING' - } - - if (!stringToLogLevel[level]) { - return level - } - - return stringToLogLevel[level] -} - -type LogData = { - message: string - splat: any[] - error?: Error -} - -class DefaultLogManager implements LogManager { - private loggers: { - [name: string]: LoggerFacade - } - private defaultLoggerFacade = new OptimizelyLogger() - - constructor() { - this.loggers = {} - } - - getLogger(name?: string): LoggerFacade { - if (!name) { - return this.defaultLoggerFacade - } - - if (!this.loggers[name]) { - this.loggers[name] = new OptimizelyLogger({ messagePrefix: name }) - } - - return this.loggers[name] - } -} - -type ConsoleLogHandlerConfig = { - logLevel?: LogLevel | string - logToConsole?: boolean - prefix?: string -} - -export class ConsoleLogHandler implements LogHandler { - public logLevel: LogLevel - private logToConsole: boolean - private prefix: string - - /** - * Creates an instance of ConsoleLogger. - * @param {ConsoleLogHandlerConfig} config - * @memberof ConsoleLogger - */ - constructor(config: ConsoleLogHandlerConfig = {}) { - this.logLevel = LogLevel.NOTSET - if (config.logLevel !== undefined && isValidEnum(LogLevel, config.logLevel)) { - this.setLogLevel(config.logLevel) - } - - this.logToConsole = config.logToConsole !== undefined ? !!config.logToConsole : true - this.prefix = config.prefix !== undefined ? config.prefix : '[OPTIMIZELY]' - } - - /** - * @param {LogLevel} level - * @param {string} message - * @memberof ConsoleLogger - */ - log(level: LogLevel, message: string) { - if (!this.shouldLog(level) || !this.logToConsole) { - return - } - - let logMessage: string = `${this.prefix} - ${this.getLogLevelName( - level, - )} ${this.getTime()} ${message}` - - this.consoleLog(level, [logMessage]) - } - - /** - * @param {LogLevel} level - * @memberof ConsoleLogger - */ - setLogLevel(level: LogLevel | string) { - level = coerceLogLevel(level) - if (!isValidEnum(LogLevel, level) || level === undefined) { - this.logLevel = LogLevel.ERROR - } else { - this.logLevel = level - } - } - - /** - * @returns {string} - * @memberof ConsoleLogger - */ - getTime(): string { - return new Date().toISOString() - } - - /** - * @private - * @param {LogLevel} targetLogLevel - * @returns {boolean} - * @memberof ConsoleLogger - */ - private shouldLog(targetLogLevel: LogLevel): boolean { - return targetLogLevel >= this.logLevel - } - - /** - * @private - * @param {LogLevel} logLevel - * @returns {string} - * @memberof ConsoleLogger - */ - private getLogLevelName(logLevel: LogLevel): string { - switch (logLevel) { - case LogLevel.DEBUG: - return 'DEBUG' - case LogLevel.INFO: - return 'INFO ' - case LogLevel.WARNING: - return 'WARN ' - case LogLevel.ERROR: - return 'ERROR' - default: - return 'NOTSET' - } - } - - /** - * @private - * @param {LogLevel} logLevel - * @param {string[]} logArguments - * @memberof ConsoleLogger - */ - private consoleLog(logLevel: LogLevel, logArguments: [string, ...string[]]) { - switch (logLevel) { - case LogLevel.DEBUG: - console.log.apply(console, logArguments) - break - case LogLevel.INFO: - console.info.apply(console, logArguments) - break - case LogLevel.WARNING: - console.warn.apply(console, logArguments) - break - case LogLevel.ERROR: - console.error.apply(console, logArguments) - break - default: - console.log.apply(console, logArguments) - } - } -} - -let globalLogLevel: LogLevel = LogLevel.NOTSET -let globalLogHandler: LogHandler | null = null - -class OptimizelyLogger implements LoggerFacade { - private messagePrefix: string = '' - - constructor(opts: { messagePrefix?: string } = {}) { - if (opts.messagePrefix) { - this.messagePrefix = opts.messagePrefix - } - } - - /** - * @param {(LogLevel | LogInputObject)} levelOrObj - * @param {string} [message] - * @memberof OptimizelyLogger - */ - log(level: LogLevel | string, message: string): void { - this.internalLog(coerceLogLevel(level), { - message, - splat: [], - }) - } - - info(message: string | Error, ...splat: any[]): void { - this.namedLog(LogLevel.INFO, message, splat) - } - - debug(message: string | Error, ...splat: any[]): void { - this.namedLog(LogLevel.DEBUG, message, splat) - } - - warn(message: string | Error, ...splat: any[]): void { - this.namedLog(LogLevel.WARNING, message, splat) - } - - error(message: string | Error, ...splat: any[]): void { - this.namedLog(LogLevel.ERROR, message, splat) - } - - private format(data: LogData): string { - return `${this.messagePrefix ? this.messagePrefix + ': ' : ''}${sprintf( - data.message, - ...data.splat, - )}` - } - - private internalLog(level: LogLevel, data: LogData): void { - if (!globalLogHandler) { - return - } - - if (level < globalLogLevel) { - return - } - - globalLogHandler.log(level, this.format(data)) - - if (data.error && data.error instanceof Error) { - getErrorHandler().handleError(data.error) - } - } - - private namedLog(level: LogLevel, message: string | Error, splat: any[]): void { - let error: Error | undefined - - if (message instanceof Error) { - error = message - message = error.message - this.internalLog(level, { - error, - message, - splat, - }) - return - } - - if (splat.length === 0) { - this.internalLog(level, { - message, - splat, - }) - return - } - - const last = splat[splat.length - 1] - if (last instanceof Error) { - error = last - splat.splice(-1) - } - - this.internalLog(level, { message, error, splat }) - } -} - -let globalLogManager: LogManager = new DefaultLogManager() - -export function getLogger(name?: string): LoggerFacade { - return globalLogManager.getLogger(name) -} - -export function setLogHandler(logger: LogHandler | null) { - globalLogHandler = logger -} - -export function setLogLevel(level: LogLevel | string) { - level = coerceLogLevel(level) - if (!isValidEnum(LogLevel, level) || level === undefined) { - globalLogLevel = LogLevel.ERROR - } else { - globalLogLevel = level - } -} - -export function getLogLevel(): LogLevel { - return globalLogLevel -} - -/** - * Resets all global logger state to it's original - */ -export function resetLogger() { - globalLogManager = new DefaultLogManager() - globalLogLevel = LogLevel.NOTSET -} diff --git a/packages/logging/src/models.ts b/packages/logging/src/models.ts deleted file mode 100644 index d8d628e08..000000000 --- a/packages/logging/src/models.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export enum LogLevel { - NOTSET = 0, - DEBUG = 1, - INFO = 2, - WARNING = 3, - ERROR = 4, -} - -export interface LoggerFacade { - log(level: LogLevel | string, message: string): void - - info(message: string | Error, ...splat: any[]): void - - debug(message: string | Error, ...splat: any[]): void - - warn(message: string | Error, ...splat: any[]): void - - error(message: string | Error, ...splat: any[]): void -} - -export interface LogManager { - getLogger(name?: string): LoggerFacade -} - -export interface LogHandler { - log(level: LogLevel, message: string): void -} \ No newline at end of file diff --git a/packages/logging/tsconfig.json b/packages/logging/tsconfig.json deleted file mode 100644 index 2ba8d7164..000000000 --- a/packages/logging/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib" - }, - "include": [ - "./src", - ], - "exclude": [ - "./lib", - "./src/**/*.spec.ts" - ] -} \ No newline at end of file diff --git a/packages/logging/yarn.lock b/packages/logging/yarn.lock deleted file mode 100644 index 1d4c46c5a..000000000 --- a/packages/logging/yarn.lock +++ /dev/null @@ -1,3689 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0-beta.35": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" - integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/highlight@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" - integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - -"@optimizely/js-sdk-utils@^0.1.0": - version "0.1.0" - resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm-local/@optimizely/js-sdk-utils/-/@optimizely/js-sdk-utils-0.1.0.tgz#4855b2dc928692010b6a86409074d0fc400ad443" - integrity sha1-SFWy3JKGkgELaoZAkHTQ/EAK1EM= - dependencies: - uuid "^3.3.2" - -"@types/jest@^23.3.12": - version "23.3.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a" - integrity sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug== - -"@types/sprintf-js@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/sprintf-js/-/sprintf-js-1.1.2.tgz#a4fcb84c7344f39f70dc4eec0e1e7f10a48597a3" - integrity sha512-hkgzYF+qnIl8uTO8rmUSVSfQ8BIfMXC4yJAF4n8BE758YsKBZvFC4NumnAegj7KmylP0liEZNpb9RRGFMbFejA== - -abab@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" - integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn-globals@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" - integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - -acorn-walk@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" - integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== - -acorn@^5.5.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" - integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== - -acorn@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" - integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== - -ajv@^6.5.5: - version "6.9.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1" - integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -append-transform@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" - integrity sha1-126/jKlNJ24keja61EpLdKthGZE= - dependencies: - default-require-extensions "^1.0.0" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= - dependencies: - arr-flatten "^1.0.1" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== - -async@^2.1.4, async@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" - integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== - dependencies: - lodash "^4.17.11" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.0.0, babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-generator@^6.18.0, babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-jest@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-23.6.0.tgz#a644232366557a2240a0c083da6b25786185a2f1" - integrity sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew== - dependencies: - babel-plugin-istanbul "^4.1.6" - babel-preset-jest "^23.2.0" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-istanbul@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" - integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ== - dependencies: - babel-plugin-syntax-object-rest-spread "^6.13.0" - find-up "^2.1.0" - istanbul-lib-instrument "^1.10.1" - test-exclude "^4.2.1" - -babel-plugin-jest-hoist@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" - integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc= - -babel-plugin-syntax-object-rest-spread@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" - integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= - -babel-preset-jest@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46" - integrity sha1-jsegOhOPABoaj7HoETZSvxpV2kY= - dependencies: - babel-plugin-jest-hoist "^23.2.0" - babel-plugin-syntax-object-rest-spread "^6.13.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -browser-process-hrtime@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" - integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== - -browser-resolve@^1.11.3: - version "1.11.3" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" - integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== - dependencies: - resolve "1.1.7" - -bs-logger@0.x: - version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" - integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== - dependencies: - fast-json-stable-stringify "2.x" - -bser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" - integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk= - dependencies: - node-int64 "^0.4.0" - -buffer-from@1.x, buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -capture-exit@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" - integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= - dependencies: - rsvp "^3.3.3" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" - integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== - -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== - dependencies: - delayed-stream "~1.0.0" - -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -convert-source-map@^1.4.0, convert-source-map@^1.5.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== - dependencies: - safe-buffer "~5.1.1" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" - integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": - version "0.3.6" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad" - integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A== - -cssstyle@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.1.tgz#3aceb2759eaf514ac1a21628d723d6043a819495" - integrity sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A== - dependencies: - cssom "0.3.x" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -data-urls@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - -debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -decamelize@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -default-require-extensions@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" - integrity sha1-836hXT4T/9m0N9M+GnW1+5eHTLg= - dependencies: - strip-bom "^2.0.0" - -define-properties@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -detect-newline@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.5.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-keys "^1.0.12" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escodegen@^1.9.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" - integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -estraverse@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= - -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== - dependencies: - merge "^1.2.0" - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= - dependencies: - is-posix-bracket "^0.1.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= - dependencies: - fill-range "^2.1.0" - -expect@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98" - integrity sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w== - dependencies: - ansi-styles "^3.2.0" - jest-diff "^23.6.0" - jest-get-type "^22.1.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= - dependencies: - is-extglob "^1.0.0" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -fast-levenshtein@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fb-watchman@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" - integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= - dependencies: - bser "^2.0.0" - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= - -fileset@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" - integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= - dependencies: - glob "^7.0.3" - minimatch "^3.0.3" - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fs-minipass@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" - integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== - dependencies: - minipass "^2.2.1" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" - integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== - dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= - dependencies: - is-glob "^2.0.0" - -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" - integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== - -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= - -handlebars@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" - integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== - dependencies: - async "^2.5.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hosted-git-info@^2.1.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" - integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== - -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -iconv-lite@0.4.24, iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== - dependencies: - minimatch "^3.0.4" - -import-local@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" - integrity sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ== - dependencies: - pkg-dir "^2.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-generator-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" - integrity sha1-lp1J4bszKfa7fwkIm+JleLLd1Go= - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= - dependencies: - is-extglob "^1.0.0" - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -istanbul-api@^1.3.1: - version "1.3.7" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa" - integrity sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA== - dependencies: - async "^2.1.4" - fileset "^2.0.2" - istanbul-lib-coverage "^1.2.1" - istanbul-lib-hook "^1.2.2" - istanbul-lib-instrument "^1.10.2" - istanbul-lib-report "^1.1.5" - istanbul-lib-source-maps "^1.2.6" - istanbul-reports "^1.5.1" - js-yaml "^3.7.0" - mkdirp "^0.5.1" - once "^1.4.0" - -istanbul-lib-coverage@^1.2.0, istanbul-lib-coverage@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" - integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== - -istanbul-lib-hook@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86" - integrity sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw== - dependencies: - append-transform "^0.4.0" - -istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" - integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.18.0" - istanbul-lib-coverage "^1.2.1" - semver "^5.3.0" - -istanbul-lib-report@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c" - integrity sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw== - dependencies: - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - path-parse "^1.0.5" - supports-color "^3.1.2" - -istanbul-lib-source-maps@^1.2.4, istanbul-lib-source-maps@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" - integrity sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg== - dependencies: - debug "^3.1.0" - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - rimraf "^2.6.1" - source-map "^0.5.3" - -istanbul-reports@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a" - integrity sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw== - dependencies: - handlebars "^4.0.3" - -jest-changed-files@^23.4.2: - version "23.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" - integrity sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA== - dependencies: - throat "^4.0.0" - -jest-cli@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.6.0.tgz#61ab917744338f443ef2baa282ddffdd658a5da4" - integrity sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.1.11" - import-local "^1.0.0" - is-ci "^1.0.10" - istanbul-api "^1.3.1" - istanbul-lib-coverage "^1.2.0" - istanbul-lib-instrument "^1.10.1" - istanbul-lib-source-maps "^1.2.4" - jest-changed-files "^23.4.2" - jest-config "^23.6.0" - jest-environment-jsdom "^23.4.0" - jest-get-type "^22.1.0" - jest-haste-map "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - jest-resolve-dependencies "^23.6.0" - jest-runner "^23.6.0" - jest-runtime "^23.6.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - jest-watcher "^23.4.0" - jest-worker "^23.2.0" - micromatch "^2.3.11" - node-notifier "^5.2.1" - prompts "^0.1.9" - realpath-native "^1.0.0" - rimraf "^2.5.4" - slash "^1.0.0" - string-length "^2.0.0" - strip-ansi "^4.0.0" - which "^1.2.12" - yargs "^11.0.0" - -jest-config@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.6.0.tgz#f82546a90ade2d8c7026fbf6ac5207fc22f8eb1d" - integrity sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ== - dependencies: - babel-core "^6.0.0" - babel-jest "^23.6.0" - chalk "^2.0.1" - glob "^7.1.1" - jest-environment-jsdom "^23.4.0" - jest-environment-node "^23.4.0" - jest-get-type "^22.1.0" - jest-jasmine2 "^23.6.0" - jest-regex-util "^23.3.0" - jest-resolve "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - micromatch "^2.3.11" - pretty-format "^23.6.0" - -jest-diff@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d" - integrity sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g== - dependencies: - chalk "^2.0.1" - diff "^3.2.0" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-docblock@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-23.2.0.tgz#f085e1f18548d99fdd69b20207e6fd55d91383a7" - integrity sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c= - dependencies: - detect-newline "^2.1.0" - -jest-each@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz#ba0c3a82a8054387016139c733a05242d3d71575" - integrity sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg== - dependencies: - chalk "^2.0.1" - pretty-format "^23.6.0" - -jest-environment-jsdom@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz#056a7952b3fea513ac62a140a2c368c79d9e6023" - integrity sha1-BWp5UrP+pROsYqFAosNox52eYCM= - dependencies: - jest-mock "^23.2.0" - jest-util "^23.4.0" - jsdom "^11.5.1" - -jest-environment-node@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-23.4.0.tgz#57e80ed0841dea303167cce8cd79521debafde10" - integrity sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA= - dependencies: - jest-mock "^23.2.0" - jest-util "^23.4.0" - -jest-get-type@^22.1.0: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" - integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== - -jest-haste-map@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.6.0.tgz#2e3eb997814ca696d62afdb3f2529f5bbc935e16" - integrity sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg== - dependencies: - fb-watchman "^2.0.0" - graceful-fs "^4.1.11" - invariant "^2.2.4" - jest-docblock "^23.2.0" - jest-serializer "^23.0.1" - jest-worker "^23.2.0" - micromatch "^2.3.11" - sane "^2.0.0" - -jest-jasmine2@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz#840e937f848a6c8638df24360ab869cc718592e0" - integrity sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ== - dependencies: - babel-traverse "^6.0.0" - chalk "^2.0.1" - co "^4.6.0" - expect "^23.6.0" - is-generator-fn "^1.0.0" - jest-diff "^23.6.0" - jest-each "^23.6.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - pretty-format "^23.6.0" - -jest-leak-detector@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz#e4230fd42cf381a1a1971237ad56897de7e171de" - integrity sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg== - dependencies: - pretty-format "^23.6.0" - -jest-matcher-utils@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80" - integrity sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog== - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-message-util@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-23.4.0.tgz#17610c50942349508d01a3d1e0bda2c079086a9f" - integrity sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8= - dependencies: - "@babel/code-frame" "^7.0.0-beta.35" - chalk "^2.0.1" - micromatch "^2.3.11" - slash "^1.0.0" - stack-utils "^1.0.1" - -jest-mock@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-23.2.0.tgz#ad1c60f29e8719d47c26e1138098b6d18b261134" - integrity sha1-rRxg8p6HGdR8JuETgJi20YsmETQ= - -jest-regex-util@^23.3.0: - version "23.3.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-23.3.0.tgz#5f86729547c2785c4002ceaa8f849fe8ca471bc5" - integrity sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U= - -jest-resolve-dependencies@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz#b4526af24c8540d9a3fab102c15081cf509b723d" - integrity sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA== - dependencies: - jest-regex-util "^23.3.0" - jest-snapshot "^23.6.0" - -jest-resolve@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.6.0.tgz#cf1d1a24ce7ee7b23d661c33ba2150f3aebfa0ae" - integrity sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA== - dependencies: - browser-resolve "^1.11.3" - chalk "^2.0.1" - realpath-native "^1.0.0" - -jest-runner@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.6.0.tgz#3894bd219ffc3f3cb94dc48a4170a2e6f23a5a38" - integrity sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA== - dependencies: - exit "^0.1.2" - graceful-fs "^4.1.11" - jest-config "^23.6.0" - jest-docblock "^23.2.0" - jest-haste-map "^23.6.0" - jest-jasmine2 "^23.6.0" - jest-leak-detector "^23.6.0" - jest-message-util "^23.4.0" - jest-runtime "^23.6.0" - jest-util "^23.4.0" - jest-worker "^23.2.0" - source-map-support "^0.5.6" - throat "^4.0.0" - -jest-runtime@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.6.0.tgz#059e58c8ab445917cd0e0d84ac2ba68de8f23082" - integrity sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw== - dependencies: - babel-core "^6.0.0" - babel-plugin-istanbul "^4.1.6" - chalk "^2.0.1" - convert-source-map "^1.4.0" - exit "^0.1.2" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.1.11" - jest-config "^23.6.0" - jest-haste-map "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - jest-resolve "^23.6.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - micromatch "^2.3.11" - realpath-native "^1.0.0" - slash "^1.0.0" - strip-bom "3.0.0" - write-file-atomic "^2.1.0" - yargs "^11.0.0" - -jest-serializer@^23.0.1: - version "23.0.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-23.0.1.tgz#a3776aeb311e90fe83fab9e533e85102bd164165" - integrity sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU= - -jest-snapshot@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.6.0.tgz#f9c2625d1b18acda01ec2d2b826c0ce58a5aa17a" - integrity sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg== - dependencies: - babel-types "^6.0.0" - chalk "^2.0.1" - jest-diff "^23.6.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-resolve "^23.6.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^23.6.0" - semver "^5.5.0" - -jest-util@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-23.4.0.tgz#4d063cb927baf0a23831ff61bec2cbbf49793561" - integrity sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE= - dependencies: - callsites "^2.0.0" - chalk "^2.0.1" - graceful-fs "^4.1.11" - is-ci "^1.0.10" - jest-message-util "^23.4.0" - mkdirp "^0.5.1" - slash "^1.0.0" - source-map "^0.6.0" - -jest-validate@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" - integrity sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A== - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - leven "^2.1.0" - pretty-format "^23.6.0" - -jest-watcher@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-23.4.0.tgz#d2e28ce74f8dad6c6afc922b92cabef6ed05c91c" - integrity sha1-0uKM50+NrWxq/JIrksq+9u0FyRw= - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - string-length "^2.0.0" - -jest-worker@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-23.2.0.tgz#faf706a8da36fae60eb26957257fa7b5d8ea02b9" - integrity sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk= - dependencies: - merge-stream "^1.0.1" - -jest@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-23.6.0.tgz#ad5835e923ebf6e19e7a1d7529a432edfee7813d" - integrity sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw== - dependencies: - import-local "^1.0.0" - jest-cli "^23.6.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.7.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsdom@^11.5.1: - version "11.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json5@2.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" - integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== - dependencies: - minimist "^1.2.0" - -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== - -kleur@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" - integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - -leven@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" - integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= - -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash@^4.17.11, lodash@^4.17.4: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-error@1.x: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" - integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== - -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= - dependencies: - tmpl "1.0.x" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" - integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= - dependencies: - mimic-fn "^1.0.0" - -merge-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" - integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= - dependencies: - readable-stream "^2.0.1" - -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - -micromatch@^2.3.11: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -mime-db@~1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.22" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" - integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== - dependencies: - mime-db "~1.38.0" - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -minimatch@^3.0.3, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.1.1, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - -minipass@^2.2.1, minipass@^2.3.4: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== - dependencies: - minipass "^2.2.1" - -mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" - integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== - dependencies: - debug "^2.1.2" - iconv-lite "^0.4.4" - sax "^1.2.4" - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= - -node-notifier@^5.2.1: - version "5.4.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a" - integrity sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ== - dependencies: - growly "^1.3.0" - is-wsl "^1.1.0" - semver "^5.5.0" - shellwords "^0.1.1" - which "^1.3.0" - -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -npm-bundled@^1.0.1: - version "1.0.6" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" - integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== - -npm-packlist@^1.1.6: - version "1.4.1" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" - integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -nwsapi@^2.0.7: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f" - integrity sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-keys@^1.0.12: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032" - integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -once@^1.3.0, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA== - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.5, path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= - -pretty-format@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" - integrity sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw== - dependencies: - ansi-regex "^3.0.0" - ansi-styles "^3.2.0" - -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== - -prompts@^0.1.9: - version "0.1.14" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2" - integrity sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w== - dependencies: - kleur "^2.0.1" - sisteransi "^0.1.1" - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -psl@^1.1.24, psl@^1.1.28: - version "1.1.31" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" - integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.1, readable-stream@^2.0.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -realpath-native@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== - dependencies: - is-equal-shallow "^0.1.3" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request-promise-core@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" - integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== - dependencies: - lodash "^4.17.11" - -request-promise-native@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" - integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== - dependencies: - request-promise-core "1.1.2" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.87.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@1.x, resolve@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== - dependencies: - path-parse "^1.0.6" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rimraf@^2.5.4, rimraf@^2.6.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rsvp@^3.3.3: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" - integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== - -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sane@^2.0.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" - integrity sha1-tNwYYcIbQn6SlQej51HiosuKs/o= - dependencies: - anymatch "^2.0.0" - capture-exit "^1.2.0" - exec-sh "^0.2.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - watch "~0.18.0" - optionalDependencies: - fsevents "^1.2.3" - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5, semver@^5.5.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" - -set-value@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" - integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -sisteransi@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" - integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-map-resolve@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" - integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== - dependencies: - atob "^2.1.1" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-support@^0.5.6: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" - integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stack-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" - integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - -string-length@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" - integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= - dependencies: - astral-regex "^1.0.0" - strip-ansi "^4.0.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-bom@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.1.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -symbol-tree@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" - integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= - -tar@^4: - version "4.4.8" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" - integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.4" - minizlib "^1.1.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.2" - -test-exclude@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" - integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== - dependencies: - arrify "^1.0.1" - micromatch "^2.3.11" - object-assign "^4.1.0" - read-pkg-up "^1.0.1" - require-main-filename "^1.0.1" - -throat@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" - integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= - -tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -tough-cookie@^2.3.3, tough-cookie@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -ts-jest@^23.10.5: - version "23.10.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.10.5.tgz#cdb550df4466a30489bf70ba867615799f388dd5" - integrity sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "10.x" - -tslib@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== - dependencies: - commander "~2.17.1" - source-map "~0.6.1" - -union-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" - integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^0.4.3" - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - -uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -w3c-hr-time@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" - integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= - dependencies: - browser-process-hrtime "^0.1.2" - -walker@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= - dependencies: - makeerror "1.0.x" - -watch@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" - integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= - dependencies: - exec-sh "^0.2.0" - minimist "^1.2.0" - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - -whatwg-url@^6.4.1: - version "6.5.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -whatwg-url@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" - integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.12, which@^1.2.9, which@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^2.1.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.2.tgz#a7181706dfba17855d221140a9c06e15fcdd87b9" - integrity sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -ws@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" - integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== - dependencies: - async-limiter "~1.0.0" - -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yallist@^3.0.0, yallist@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== - -yargs-parser@10.x: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - -yargs-parser@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" - integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc= - dependencies: - camelcase "^4.1.0" - -yargs@^11.0.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" - integrity sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A== - dependencies: - cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^9.0.2" diff --git a/packages/optimizely-sdk/.prettierrc b/packages/optimizely-sdk/.prettierrc deleted file mode 100644 index 62301e2e3..000000000 --- a/packages/optimizely-sdk/.prettierrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": true, - "trailingComma": "es5", - "bracketSpacing": true, - "jsxBracketSameLine": false -} diff --git a/packages/optimizely-sdk/CHANGELOG.MD b/packages/optimizely-sdk/CHANGELOG.MD deleted file mode 100644 index bdf50c55c..000000000 --- a/packages/optimizely-sdk/CHANGELOG.MD +++ /dev/null @@ -1,355 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## [Unreleased] -Changes that have landed but are not yet released. - -## [3.1.0-beta1] - March 5th, 2019 - -### Changed - -- New APIs for setting `logger` and `logLevel` on the optimizelySDK singleton ([#232](https://github.com/optimizely/javascript-sdk/pull/232)) -- `logger` and `logLevel` are now set globally for all instances of Optimizely. If you were passing -different loggers to individual instances of Optimizely, logging behavior may now be different. - -#### Setting a ConsoleLogger - -```js -var optimizelySDK = require('@optimizely/optimizely-sdk') - -// logger and logLevel are now set on the optimizelySDK singleton -optimizelySDK.setLogger(optimizelySDK.logging.createLogger()) - -// valid levels: 'DEBUG', 'INFO', 'WARN', 'ERROR' -optimizelySDK.setLogLevel('WARN') -// enums can also be used -optimizelySDK.setLogLevel(optimizely.enums.LOG_LEVEL.ERROR) -``` - -#### Disable logging - -```js -var optimizelySDK = require('@optimizely/optimizely-sdk') - -optimizelySDK.setLogger(null) -``` - -## [3.0.1] - February 21, 2019 - -### Changed -- Expose default `loggers`, `errorHandlers`, `eventDispatcher` and `enums` on top level require. -- `createLogger` and `createNoOpLogger` are available as methods on `optimizelySdk.logging` -- Added `optimizelySdk.errorHandler` -- Added `optimizelySdk.eventDispatcher` -- Added `optimizelySdk.enums` - -## [3.0.0] - February 13, 2019 - -The 3.0 release improves event tracking and supports additional audience targeting functionality. - -### New Features: - -- Event tracking ([#207](https://github.com/optimizely/javascript-sdk/pull/207)): - - The `track` method now dispatches its conversion event _unconditionally_, without first determining whether the user is targeted by a known experiment that uses the event. This may increase outbound network traffic. - - In Optimizely results, conversion events sent by 3.0 SDKs are automatically attributed to variations that the user has previously seen, as long as our backend has actually received the impression events for those variations. - - Altogether, this allows you to track conversion events and attribute them to variations even when you don't know all of a user's attribute values, and even if the user's attribute values or the experiment's configuration have changed such that the user is no longer affected by the experiment. As a result, **you may observe an increase in the conversion rate for previously-instrumented events.** If that is undesirable, you can reset the results of previously-running experiments after upgrading to the 3.0 SDK. - - This will also allow you to attribute events to variations from other Optimizely projects in your account, even though those experiments don't appear in the same datafile. - - Note that for results segmentation in Optimizely results, the user attribute values from one event are automatically applied to all other events in the same session, as long as the events in question were actually received by our backend. This behavior was already in place and is not affected by the 3.0 release. -- Support for all types of attribute values, not just strings ([#174](https://github.com/optimizely/javascript-sdk/pull/174), [#204](https://github.com/optimizely/javascript-sdk/pull/204)). - - All values are passed through to notification listeners. - - Strings, booleans, and valid numbers are passed to the event dispatcher and can be used for Optimizely results segmentation. A valid number is a finite number in the inclusive range [-2⁵³, 2⁵³]. - - Strings, booleans, and valid numbers are relevant for audience conditions. -- Support for additional matchers in audience conditions ([#174](https://github.com/optimizely/javascript-sdk/pull/174)): - - An `exists` matcher that passes if the user has a non-null value for the targeted user attribute and fails otherwise. - - A `substring` matcher that resolves if the user has a string value for the targeted attribute. - - `gt` (greater than) and `lt` (less than) matchers that resolve if the user has a valid number value for the targeted attribute. A valid number is a finite number in the inclusive range [-2⁵³, 2⁵³]. - - The original (`exact`) matcher can now be used to target booleans and valid numbers, not just strings. -- Support for A/B tests, feature tests, and feature rollouts whose audiences are combined using `"and"` and `"not"` operators, not just the `"or"` operator ([#175](https://github.com/optimizely/javascript-sdk/pull/175)) -- Updated Pull Request template and commit message guidelines ([#183](https://github.com/optimizely/javascript-sdk/pull/183)). -- Support for sticky bucketing. You can pass an `$opt_experiment_bucket_map` attribute to ensure that the user gets a specific variation ([#179](https://github.com/optimizely/javascript-sdk/pull/179)). -- Support for bucketing IDs when evaluating feature rollouts, not just when evaluating A/B tests and feature tests ([#200](https://github.com/optimizely/javascript-sdk/pull/200)). -- TypeScript declarations ([#199](https://github.com/optimizely/javascript-sdk/pull/199)). - -### Breaking Changes: - -- Previously, notification listeners were only given string-valued user attributes because only strings could be passed into various method calls. That is no longer the case. You may pass non-string attribute values, and if you do, you must update your notification listeners to be able to receive whatever values you pass in ([#174](https://github.com/optimizely/javascript-sdk/pull/174), [#204](https://github.com/optimizely/javascript-sdk/pull/204)). -- Drops `window.optimizelyClient` from the bundled build. Now, `window.optimizelySdk` can be used instead. ([#189](https://github.com/optimizely/javascript-sdk/pull/189)). - -### Bug Fixes: - -- Experiments and features can no longer activate when a negatively targeted attribute has a missing, null, or malformed value ([#174](https://github.com/optimizely/javascript-sdk/pull/174)). - - Audience conditions (except for the new `exists` matcher) no longer resolve to `false` when they fail to find an legitimate value for the targeted user attribute. The result remains `null` (unknown). Therefore, an audience that negates such a condition (using the `"not"` operator) can no longer resolve to `true` unless there is an unrelated branch in the condition tree that itself resolves to `true`. -- `setForcedVariation` now treats an empty variation key as invalid and does not reset the variation ([#185](https://github.com/optimizely/javascript-sdk/pull/185)). -- You can now specify `0` as the `revenue` or `value` for a conversion event when using the `track` method. Previously, `0` was withheld and would not appear in your data export ([#213](https://github.com/optimizely/javascript-sdk/pull/213)). -- The existence of a feature test in an experimentation group no longer causes A/B tests in the same group to activate the same feature ([#194](https://github.com/optimizely/fullstack-sdk-compatibility-suite/pull/194)). - -## [2.3.1] - November 14, 2018 - -### Fixed - -- fix(bundling): Publish the unminified UMD bundle along with the minified one. ([#187](https://github.com/optimizely/javascript-sdk/pull/187)) - -## [2.3.0] - November 14, 2018 - -### New Features -* Allow sticky bucketing via passing in `attributes.$opt_experiment_bucket_map`, this more easily allows customers to do some async data fetching and ensure a user gets a specific variation. - -``` -const userId = '123' -const expId = '456' -const variationId = '678' -const userAttributes = { - $opt_experiment_bucket_map: { - [expId]: { - variation_id: variationId - } - } -} - -var selectedVariationKey = optimizelyClient.activate('experiment-1', userId, userAttributes); -``` - -## [2.2.0] - September 26, 2018 - -### Fixed -- Track and activate should not remove null attributes ([#168](https://github.com/optimizely/javascript-sdk/pull/168)) -- Track attributes with valid attribute types ([#166](https://github.com/optimizely/javascript-sdk/pull/166)) -- Prevent SDK from initializing if the datafile version in invalid ([#161](https://github.com/optimizely/javascript-sdk/pull/161)) -- Updating lerna to latest version ([#160](https://github.com/optimizely/javascript-sdk/pull/160)) - -### Changed -- Change invalid experiment key to debug level ([#165](https://github.com/optimizely/javascript-sdk/pull/165)) - -## [2.1.3] - August 21, 2018 - -### Fixed -- Send all decisions for the same event in one snapshot. ([#155](https://github.com/optimizely/javascript-sdk/pull/155)) -- Give Node.js consumers the unbundled package ([#133](https://github.com/optimizely/javascript-sdk/pull/133)) - -### Deprecated -- The UMD build of the SDK now assigns the SDK namespace object to `window.optimizelySdk` rather than to `window.optimizelyClient`. The old name still works, but on its first access a deprecation warning is logged to the console. The alias will be removed in the 3.0.0 release. ([#152](https://github.com/optimizely/javascript-sdk/pull/152)) - -## [2.1.2] - June 25, 2018 - -### Fixed -- Failure to log success message when event dispatched ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) -- Fix: Don't call success message when event fails to send ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) - -## [2.0.5] - June 25, 2018 - -### Fixed -- Failure to log success message when event dispatched ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) -- Fix: Don't call success message when event fails to send ([#123](https://github.com/optimizely/javascript-sdk/pull/123)) - -## 2.1.1 -June 19, 2018 - -* Fix: send impression event for Feature Test with Feature disabled ([#117](https://github.com/optimizely/javascript-sdk/pull/117)) - -## 2.0.4 -June 19, 2018 - -* Fix: send impression event for Feature Test with Feature disabled ([#117](https://github.com/optimizely/javascript-sdk/pull/117)) - -## 2.1.0 -May 24, 2018 - -* Introduces support for bot filtering. - -## 2.0.3 -May 24, 2018 - -* Remove [`request`](https://www.npmjs.com/package/request) dependency ([#98](https://github.com/optimizely/javascript-sdk/pull/98)) -* Add package-lock.json ([#100](https://github.com/optimizely/javascript-sdk/pull/100)) -* Input validation in Activate, Track, and GetVariation methods ([#91](https://github.com/optimizely/javascript-sdk/pull/91) by [@mfahadahmed](https://github.com/mfahadahmed)) - -## 2.0.1 -April 16th, 2018 - -* Improve browser entry point by pointing to the browser index file instead of the webpack-compiled bundle. ([@DullReferenceException](https://github.com/DullReferenceException) in [#88](https://github.com/optimizely/javascript-sdk/pull/88)) - -## 2.0.0 -April 11th, 2018 - -This major release of the Optimizely SDK introduces APIs for Feature Management. It also introduces some breaking changes listed below. - -### New Features -* Introduces the `isFeatureEnabled` API to determine whether to show a feature to a user or not. -``` -var enabled = optimizelyClient.isFeatureEnabled('my_feature_key', 'user_1', userAttributes); -``` - -* You can also get all the enabled features for the user by calling the following method which returns a list of strings representing the feature keys: -``` -var enabledFeatures = optimizelyClient.getEnabledFeatures('user_1', userAttributes); -``` - -* Introduces Feature Variables to configure or parameterize your feature. There are four variable types: `Integer`, `String`, `Double`, `Boolean`. -``` -var stringVariable = optimizelyClient.getFeatureVariableString('my_feature_key', 'string_variable_key', 'user_1'); -var integerVariable = optimizelyClient.getFeatureVariableInteger('my_feature_key', 'integer_variable_key', 'user_1'); -var doubleVariable = optimizelyClient.getFeatureVariableDouble('my_feature_key', 'double_variable_key', 'user_1'); -var booleanVariable = optimizelyClient.getFeatureVariableBoolean('my_feature_key', 'boolean_variable_key', 'user_1'); -``` - -### Breaking changes -* The `track` API with revenue value as a stand-alone parameter has been removed. The revenue value should be passed in as an entry of the event tags map. The key for the revenue tag is `revenue` and will be treated by Optimizely as the key for analyzing revenue data in results. -``` -var eventTags = { - 'revenue': 1200 -}; - -optimizelyClient.track('event_key', 'user_id', userAttributes, eventTags); -``` -* The package name has changed from `optimizely-client-sdk` to `optimizely-sdk` as we have consolidated both Node and JavaScript SDKs into one. - -## 2.0.0-beta1 -March 29th, 2018 - -This major release of the Optimizely SDK introduces APIs for Feature Management. It also introduces some breaking changes listed below. - -### New Features -* Introduces the `isFeatureEnabled` API to determine whether to show a feature to a user or not. -``` -var enabled = optimizelyClient.isFeatureEnabled('my_feature_key', 'user_1', userAttributes); -``` - -* You can also get all the enabled features for the user by calling the following method which returns a list of strings representing the feature keys: -``` -var enabledFeatures = optimizelyClient.getEnabledFeatures('user_1', userAttributes); -``` - -* Introduces Feature Variables to configure or parameterize your feature. There are four variable types: `Integer`, `String`, `Double`, `Boolean`. -``` -var stringVariable = optimizelyClient.getFeatureVariableString('my_feature_key', 'string_variable_key', 'user_1'); -var integerVariable = optimizelyClient.getFeatureVariableInteger('my_feature_key', 'integer_variable_key', 'user_1'); -var doubleVariable = optimizelyClient.getFeatureVariableDouble('my_feature_key', 'double_variable_key', 'user_1'); -var booleanVariable = optimizelyClient.getFeatureVariableBoolean('my_feature_key', 'boolean_variable_key', 'user_1'); -``` - -### Breaking changes -* The `track` API with revenue value as a stand-alone parameter has been removed. The revenue value should be passed in as an entry of the event tags map. The key for the revenue tag is `revenue` and will be treated by Optimizely as the key for analyzing revenue data in results. -``` -var eventTags = { - 'revenue': 1200 -}; - -optimizelyClient.track('event_key', 'user_id', userAttributes, eventTags); -``` -* The package name has changed from `optimizely-client-sdk` to `optimizely-sdk` as we have consolidated both Node and JavaScript SDKs into one. - -## 1.6.0 - -* Bump optimizely-server-sdk to version 1.5.0, which includes: - - Implemented IP anonymization. - - Implemented bucketing IDs. - - Implemented notification listeners. - -## 1.5.1 -* Bump optimizely-server-sdk to version 1.4.2, which includes: - - Bug fix to filter out undefined values in attributes and event tags - - Remove a duplicated test - -## 1.5.0 -* Bump optimizely-server-sdk to version 1.4.0, which includes: - - Add support for numeric metrics. - - Add getForcedVariation and setForcedVariation methods for client-side variation setting - - Bug fix for filtering out null attribute and event tag values - -## 1.4.3 -* Default skipJSONValidation to true -* Bump optimizely-server-sdk to version 1.3.3, which includes: - - Removed JSON Schema Validator from Optimizely constructor - - Updated SDK to use new event endpoint - - Minor bug fixes - -## 1.4.2 -* Minor performance improvements. - -## 1.4.1 -* Switched to karma/browserstack for cross-browser testing -* Removed es6-promise -* Bump optimizely-server-sdk to version 1.3.1, which includes: - - Minor performance improvements. - -## 1.4.0 -* Reduce lodash footprint. -* Bump optimizely-server-sdk to version 1.3.0, which includes: - - Introduced user profile service. - - Minor performance and readibility improvements. - -## 1.3.5 -* Bump optimizely-server-sdk to version 1.2.3, which includes: - - Switched to json-schema library which has a smaller footprint. - - Refactored order of bucketing logic. - - Refactor lodash dependencies. - - Fixed error on validation for objects with undefined values for attributes. - -## 1.3.4 -* Bump optimizely-server-sdk to version 1.2.2, which includes: - - Use the 'name' field for tracking event tags instead of 'id'. - -## 1.3.3 -* Include index.js in package.json files to make sure it gets published regardless of node environment. - -## 1.3.2 -* Bump to 1.3.2 to re-publish to npm - -## 1.3.1 -* Bump optimizely-server-sdk to version 1.2.1, which includes: - - Gracefully handle empty traffic allocation ranges. - -## 1.3.0 -* Bump optimizely-server-sdk to version 1.2.0, which includes: - - Introduce support for event tags. - - Add optional eventTags argument to track method signature. - - Removed optional eventValue argument from track method signature. - - Removed optional sessionId argument from activate and track method signatures. - - Allow log level config on createInstance method. - -## 1.2.2 -* Remove .npmignore to consolidate with .gitignore. -* Add dist and lib directories to "files" in package.json. - -## 1.2.1 -* Fix webpack build error. - -## 1.2.0 -* Bump optimizely-server-sdk to version 1.1.0, which includes: - - Add optional sessionId argument to activate and track method signatures. - - Add sessionId and revision to event ticket. - - Add 'Launched' status where user gets bucketed but event is not sent to Optimizely. - -## 1.1.1 -* Bump to optimizely-server-sdk to version 1.0.1, which includes: - - Fix bug so conversion event is not sent if user is not bucketed into any experiment. - - Bump bluebird version from 3.3.5 to 3.4.6. - - Update event endpoint from p13nlog.dz.optimizely to logx.optimizely. - -## 1.1.0 -* Add global variable name export for use in non-CommonJS environments -* Remove redundant lodash core dependency to reduce bundle bloat - -## 1.0.0 -* Introduce support for Full Stack projects in Optimizely X with no breaking changes from previous version. -* Introduce more graceful exception handling in instantiation and core methods. -* Update whitelisting to take precedence over audience condition evaluation. -* Fix bug activating/tracking with attributes not in the datafile. - -## 0.1.4 -* Add functionality for New Optimizely endpoint. - -## 0.1.3 -* Add environment detection to event builder so it can distinguish between events sent from node or the browser. - -## 0.1.2 -* Add CORS param to prevent browsers from logging cors errors in the console when dispatching events. - -## 0.1.1 -* Remove percentageIncluded field from JSON schema, which is not needed. - -## 0.1.0 -* Beta release of the Javascript SDK for our Optimizely testing solution diff --git a/packages/optimizely-sdk/README.md b/packages/optimizely-sdk/README.md deleted file mode 100644 index 4a2e273d4..000000000 --- a/packages/optimizely-sdk/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# JavaScript SDK for Optimizely X Full Stack -[![npm](https://img.shields.io/npm/v/%40optimizely%2Foptimizely-sdk.svg)](https://www.npmjs.com/package/@optimizely/optimizely-sdk) -[![npm](https://img.shields.io/npm/dm/%40optimizely%2Foptimizely-sdk.svg)](https://www.npmjs.com/package/@optimizely/optimizely-sdk) -[![Travis CI](https://img.shields.io/travis/optimizely/javascript-sdk.svg)](https://travis-ci.org/optimizely/javascript-sdk) -[![Coveralls](https://img.shields.io/coveralls/optimizely/javascript-sdk.svg)](https://coveralls.io/github/optimizely/javascript-sdk) -[![license](https://img.shields.io/github/license/optimizely/javascript-sdk.svg)](https://choosealicense.com/licenses/apache-2.0/) - - -Optimizely X Full Stack is A/B testing and feature management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at the [landing page](https://www.optimizely.com/products/full-stack/), or see the [documentation](https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=node). - -This directory contains the source code for the JavaScript SDK, which is usable in Node.js, browsers, and beyond. - -## Getting Started - -### Prerequisites - -Ensure the SDK supports all of the platforms you're targeting. In particular, the SDK targets any ES5-compliant JavaScript environment. We officially support: -- Node.js >= 4.0.0. By extension, environments like AWS Lambda, Google Cloud Functions, and Auth0 Webtasks are supported as well. Older Node.js releases likely work too (try `npm test` to validate for yourself), but are not formally supported. -- [Web browsers](https://caniuse.com/#feat=es5) - -Other environments likely are compatible, too, but note that we don't officially support them: -- Progressive Web Apps, WebViews, and hybrid mobile apps like those built with React Native and Apache Cordova. -- [Cloudflare Workers](https://developers.cloudflare.com/workers/) and [Fly](https://fly.io/), both of which are powered by recent releases of V8. -- Anywhere else you can think of that might embed a JavaScript engine. The sky is the limit; experiment everywhere! 🚀 - -Once you've validated that the SDK supports the platforms you're targeting, fetch the package from [NPM](https://www.npmjs.com/package/@optimizely/optimizely-sdk). Using `npm`: - -``` -npm install --save @optimizely/optimizely-sdk -``` - -### Usage -See the Optimizely X Full Stack [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set up your first JavaScript project and use the SDK. - -The package's entry point is a CommonJS module, which can be used directly in environments which support it (e.g., Node.js, or loaded in a browser via Browserify or RequireJS). Additionally, you can include a standalone bundle of the SDK in your web page by fetching it from [unpkg](https://unpkg.com/): - -```html -<script src="https://unpkg.com/@optimizely/optimizely-sdk/dist/optimizely.browser.umd.min.js"></script> - -<!-- You can also use the unminified version if necessary --> -<script src="https://unpkg.com/@optimizely/optimizely-sdk/dist/optimizely.browser.umd.js"></script> -``` - -When evaluated, that bundle assigns the SDK's exports to `window.optimizelySdk`. If you wish to use the asset locally (for example, if unpkg is down), you can find it in your local copy of the package at dist/optimizely.browser.umd.min.js. - -Regarding `EventDispatcher`s: In Node.js and browser environments, the default `EventDispatcher` is powered by the [`http/s`](https://nodejs.org/api/http.html) modules and by [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#Browser_compatibility), respectively. In all other environments, you must supply your own `EventDispatcher`. - -### Migrating from 1.x.x - -This version represents a major version change and, as such, introduces some breaking changes: - -- The Node.js SDK is now combined with the JavaScript SDK. We now have just one package, `@optimizely/optimizely-sdk`, that works in many JavaScript environments. - -- We no longer support Node.js < 4.0.0, which collectively [reached end-of-life](https://github.com/nodejs/Release#end-of-life-releases) on 2016-12-31. - -- You will no longer be able to pass in `revenue` value as a stand-alone argument to the `track` call. Instead you will need to pass it as an entry in the [`eventTags`](https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=javascript#event-tags). - -### Feature Management access - -To access Feature Management in the Optimizely web application, please contact your Optimizely account executive. - -## Contributing -This information is relevant only if you plan on contributing to the SDK itself. - -```sh -# Prerequisite: Install dependencies. -npm install - -# Run unit tests with mocha. -npm test - -# Run unit tests in many browsers, currently via BrowserStack. -# For this to work, the following environment variables must be set: -# - BROWSER_STACK_USERNAME -# - BROWSER_STACK_PASSWORD -npm run test-xbrowser -``` - -[.travis.yml](/.travis.yml) contains the definitions for `BROWSER_STACK_USERNAME` and `BROWSER_STACK_ACCESS_KEY` used in CI. These values are Optimizely's BrowserStack credentials, encrypted with our Travis CI public key. These creds can be rotated by following [these docs](https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml). - -## Credits - -First-party code (under lib/) is copyright Optimizely, Inc. and contributors, licensed under Apache 2.0. - -## Additional Code - -Prod dependencies are as follows: - -```json -{ - "json-schema@0.2.3": { - "licenses": [ - "AFLv2.1", - "BSD" - ], - "publisher": "Kris Zyp", - "repository": "https://github.com/kriszyp/json-schema" - }, - "lodash@4.17.10": { - "licenses": "MIT", - "publisher": "John-David Dalton", - "repository": "https://github.com/lodash/lodash" - }, - "murmurhash@0.0.2": { - "licenses": "MIT*", - "repository": "https://github.com/perezd/node-murmurhash" - }, - "sprintf@0.1.5": { - "licenses": "BSD-3-Clause", - "publisher": "Moritz Peters", - "repository": "https://github.com/maritz/node-sprintf" - }, - "uuid@3.2.1": { - "licenses": "MIT", - "repository": "https://github.com/kelektiv/node-uuid" - } -} -``` - -To regenerate this, run the following command: - -```sh -npx license-checker --production --json | jq 'map_values({ licenses, publisher, repository }) | del(.[][] | nulls)' -``` - -and remove the self (`@optimizely/optimizely-sdk`) entry. diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/index.js b/packages/optimizely-sdk/lib/core/audience_evaluator/index.js deleted file mode 100644 index a534e796b..000000000 --- a/packages/optimizely-sdk/lib/core/audience_evaluator/index.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright 2016, 2018-2019 Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var conditionTreeEvaluator = require('../condition_tree_evaluator'); -var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator'); -var enums = require('../../utils/enums'); -var sprintf = require('sprintf-js').sprintf; - -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MODULE_NAME = 'AUDIENCE_EVALUATOR'; - -module.exports = { - /** - * Determine if the given user attributes satisfy the given audience conditions - * @param {Array|String|null|undefined} audienceConditions Audience conditions to match the user attributes against - can be an array - * of audience IDs, a nested array of conditions, or a single leaf condition. - * Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"], "1" - * @param {Object} audiencesById Object providing access to full audience objects for audience IDs - * contained in audienceConditions. Keys should be audience IDs, values - * should be full audience objects with conditions properties - * @param {Object} [userAttributes] User attributes which will be used in determining if audience conditions - * are met. If not provided, defaults to an empty object - * @param {Object} logger Logger instance. - * @return {Boolean} true if the user attributes match the given audience conditions, false - * otherwise - */ - evaluate: function(audienceConditions, audiencesById, userAttributes, logger) { - // if there are no audiences, return true because that means ALL users are included in the experiment - if (!audienceConditions || audienceConditions.length === 0) { - return true; - } - - if (!userAttributes) { - userAttributes = {}; - } - - var evaluateConditionWithUserAttributes = function(condition) { - return customAttributeConditionEvaluator.evaluate(condition, userAttributes, logger); - }; - - var evaluateAudience = function(audienceId) { - var audience = audiencesById[audienceId]; - if (audience) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions))); - var result = conditionTreeEvaluator.evaluate(audience.conditions, evaluateConditionWithUserAttributes); - var resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase(); - logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText)); - return result; - } - - return null; - }; - - return conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience) || false; - }, -}; diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js b/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js deleted file mode 100644 index 8551940ca..000000000 --- a/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Copyright 2016, 2018-2019 Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var audienceEvaluator = require('./'); -var chai = require('chai'); -var sprintf = require('sprintf-js').sprintf; -var conditionTreeEvaluator = require('../condition_tree_evaluator'); -var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator'); -var sinon = require('sinon'); -var assert = chai.assert; -var logger = require('../../plugins/logger'); -var enums = require('../../utils/enums'); -var LOG_LEVEL = enums.LOG_LEVEL; - -var chromeUserAudience = { - conditions: ['and', { - name: 'browser_type', - value: 'chrome', - type: 'custom_attribute', - }], -}; -var iphoneUserAudience = { - conditions: ['and', { - name: 'device_model', - value: 'iphone', - type: 'custom_attribute', - }], -}; -var conditionsPassingWithNoAttrs = ['not', { - match: 'exists', - name: 'input_value', - type: 'custom_attribute', -}]; -var conditionsPassingWithNoAttrsAudience = { - conditions: conditionsPassingWithNoAttrs, -}; -var audiencesById = { - 0: chromeUserAudience, - 1: iphoneUserAudience, - 2: conditionsPassingWithNoAttrsAudience, -}; - -describe('lib/core/audience_evaluator', function() { - describe('APIs', function() { - describe('evaluate', function() { - var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - - beforeEach(function () { - sinon.stub(mockLogger, 'log'); - }); - - afterEach(function() { - mockLogger.log.restore(); - }); - - it('should return true if there are no audiences', function() { - assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}, mockLogger)); - }); - - it('should return false if there are audiences but no attributes', function() { - assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, {}, mockLogger)); - }); - - it('should return true if any of the audience conditions are met', function() { - var iphoneUsers = { - 'device_model': 'iphone', - }; - - var chromeUsers = { - 'browser_type': 'chrome', - }; - - var iphoneChromeUsers = { - 'browser_type': 'chrome', - 'device_model': 'iphone', - }; - - assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers, mockLogger)); - assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers, mockLogger)); - assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers, mockLogger)); - }); - - it('should return false if none of the audience conditions are met', function() { - var nexusUsers = { - 'device_model': 'nexus5', - }; - - var safariUsers = { - 'browser_type': 'safari', - }; - - var nexusSafariUsers = { - 'browser_type': 'safari', - 'device_model': 'nexus5', - }; - - assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers, mockLogger)); - assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers, mockLogger)); - assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers, mockLogger)); - }); - - it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', function() { - assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null, mockLogger)); - }); - - describe('complex audience conditions', function() { - it('should return true if any of the audiences in an "OR" condition pass', function() { - var result = audienceEvaluator.evaluate( - ['or', '0', '1'], - audiencesById, - { browser_type: 'chrome' }, - mockLogger - ); - assert.isTrue(result); - }); - - it('should return true if all of the audiences in an "AND" condition pass', function() { - var result = audienceEvaluator.evaluate( - ['and', '0', '1'], - audiencesById, - { browser_type: 'chrome', device_model: 'iphone' }, - mockLogger - ); - assert.isTrue(result); - }); - - it('should return true if the audience in a "NOT" condition does not pass', function() { - var result = audienceEvaluator.evaluate( - ['not', '1'], - audiencesById, - { device_model: 'android' }, - mockLogger - ); - assert.isTrue(result); - }); - - }); - - describe('integration with dependencies', function() { - var sandbox = sinon.sandbox.create(); - - beforeEach(function() { - sandbox.stub(conditionTreeEvaluator, 'evaluate'); - sandbox.stub(customAttributeConditionEvaluator, 'evaluate'); - }); - - afterEach(function() { - sandbox.restore(); - }); - - it('returns true if conditionTreeEvaluator.evaluate returns true', function() { - conditionTreeEvaluator.evaluate.returns(true); - var result = audienceEvaluator.evaluate( - ['or', '0', '1'], - audiencesById, - { browser_type: 'chrome' }, - mockLogger - ); - assert.isTrue(result); - }); - - it('returns false if conditionTreeEvaluator.evaluate returns false', function() { - conditionTreeEvaluator.evaluate.returns(false); - var result = audienceEvaluator.evaluate( - ['or', '0', '1'], - audiencesById, - { browser_type: 'safari' }, - mockLogger - ); - assert.isFalse(result); - }); - - it('returns false if conditionTreeEvaluator.evaluate returns null', function() { - conditionTreeEvaluator.evaluate.returns(null); - var result = audienceEvaluator.evaluate( - ['or', '0', '1'], - audiencesById, - { state: 'California' }, - mockLogger - ); - assert.isFalse(result); - }); - - it('calls customAttributeConditionEvaluator.evaluate in the leaf evaluator for audience conditions', function() { - conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { - return leafEvaluator(conditions[1]); - }); - customAttributeConditionEvaluator.evaluate.returns(false); - var userAttributes = { device_model: 'android' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); - sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); - sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); - assert.isFalse(result); - }); - }); - - describe('Audience evaluation logging', function() { - var sandbox = sinon.sandbox.create(); - - beforeEach(function() { - sandbox.stub(conditionTreeEvaluator, 'evaluate'); - sandbox.stub(customAttributeConditionEvaluator, 'evaluate'); - }); - - afterEach(function() { - sandbox.restore(); - }); - - it('logs correctly when conditionTreeEvaluator.evaluate returns null', function() { - conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { - return leafEvaluator(conditions[1]); - }); - customAttributeConditionEvaluator.evaluate.returns(null); - var userAttributes = { device_model: 5.5 }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); - sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); - sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); - assert.isFalse(result); - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'AUDIENCE_EVALUATOR: Starting to evaluate audience "1" with conditions: ["and",{"name":"device_model","value":"iphone","type":"custom_attribute"}].'); - assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to UNKNOWN.'); - }); - - it('logs correctly when conditionTreeEvaluator.evaluate returns true', function() { - conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { - return leafEvaluator(conditions[1]); - }); - customAttributeConditionEvaluator.evaluate.returns(true); - var userAttributes = { device_model: 'iphone' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); - sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); - sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); - assert.isTrue(result); - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'AUDIENCE_EVALUATOR: Starting to evaluate audience "1" with conditions: ["and",{"name":"device_model","value":"iphone","type":"custom_attribute"}].'); - assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to TRUE.'); - }); - - it('logs correctly when conditionTreeEvaluator.evaluate returns false', function() { - conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) { - return leafEvaluator(conditions[1]); - }); - customAttributeConditionEvaluator.evaluate.returns(false); - var userAttributes = { device_model: 'android' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); - sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); - sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); - assert.isFalse(result); - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'AUDIENCE_EVALUATOR: Starting to evaluate audience "1" with conditions: ["and",{"name":"device_model","value":"iphone","type":"custom_attribute"}].'); - assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to FALSE.'); - }); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/core/bucketer/index.js b/packages/optimizely-sdk/lib/core/bucketer/index.js deleted file mode 100644 index 964bbc423..000000000 --- a/packages/optimizely-sdk/lib/core/bucketer/index.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright 2016, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Bucketer API for determining the variation id from the specified parameters - */ -var enums = require('../../utils/enums'); -var murmurhash = require('murmurhash'); -var sprintf = require('sprintf-js').sprintf; - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var HASH_SEED = 1; -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MAX_HASH_VALUE = Math.pow(2, 32); -var MAX_TRAFFIC_VALUE = 10000; -var MODULE_NAME = 'BUCKETER'; -var RANDOM_POLICY = 'random'; - -module.exports = { - /** - * Determines ID of variation to be shown for the given input params - * @param {Object} bucketerParams - * @param {string} bucketerParams.experimentId - * @param {string} bucketerParams.experimentKey - * @param {string} bucketerParams.userId - * @param {Object[]} bucketerParams.trafficAllocationConfig - * @param {Array} bucketerParams.experimentKeyMap - * @param {Object} bucketerParams.groupIdMap - * @param {Object} bucketerParams.variationIdMap - * @param {string} bucketerParams.varationIdMap[].key - * @param {Object} bucketerParams.logger - * @param {string} bucketerParams.bucketingId - * @return Variation ID that user has been bucketed into, null if user is not bucketed into any experiment - */ - bucket: function(bucketerParams) { - // Check if user is in a random group; if so, check if user is bucketed into a specific experiment - var experiment = bucketerParams.experimentKeyMap[bucketerParams.experimentKey]; - var groupId = experiment['groupId']; - if (groupId) { - var group = bucketerParams.groupIdMap[groupId]; - if (!group) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_GROUP_ID, MODULE_NAME, groupId)); - } - if (group.policy === RANDOM_POLICY) { - var bucketedExperimentId = module.exports.bucketUserIntoExperiment(group, - bucketerParams.bucketingId, - bucketerParams.userId, - bucketerParams.logger); - - // Return if user is not bucketed into any experiment - if (bucketedExperimentId === null) { - var notbucketedInAnyExperimentLogMessage = sprintf(LOG_MESSAGES.USER_NOT_IN_ANY_EXPERIMENT, MODULE_NAME, bucketerParams.userId, groupId); - bucketerParams.logger.log(LOG_LEVEL.INFO, notbucketedInAnyExperimentLogMessage); - return null; - } - - // Return if user is bucketed into a different experiment than the one specified - if (bucketedExperimentId !== bucketerParams.experimentId) { - var notBucketedIntoExperimentOfGroupLogMessage = sprintf(LOG_MESSAGES.USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, MODULE_NAME, bucketerParams.userId, bucketerParams.experimentKey, groupId); - bucketerParams.logger.log(LOG_LEVEL.INFO, notBucketedIntoExperimentOfGroupLogMessage); - return null; - } - - // Continue bucketing if user is bucketed into specified experiment - var bucketedIntoExperimentOfGroupLogMessage = sprintf(LOG_MESSAGES.USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, MODULE_NAME, bucketerParams.userId, bucketerParams.experimentKey, groupId); - bucketerParams.logger.log(LOG_LEVEL.INFO, bucketedIntoExperimentOfGroupLogMessage); - } - } - var bucketingId = sprintf('%s%s', bucketerParams.bucketingId, bucketerParams.experimentId); - var bucketValue = module.exports._generateBucketValue(bucketingId); - - var bucketedUserLogMessage = sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_VARIATION_BUCKET, MODULE_NAME, bucketValue, bucketerParams.userId); - bucketerParams.logger.log(LOG_LEVEL.DEBUG, bucketedUserLogMessage); - - var entityId = module.exports._findBucket(bucketValue, bucketerParams.trafficAllocationConfig); - if (entityId === null) { - var userHasNoVariationLogMessage = sprintf(LOG_MESSAGES.USER_HAS_NO_VARIATION, MODULE_NAME, bucketerParams.userId, bucketerParams.experimentKey); - bucketerParams.logger.log(LOG_LEVEL.DEBUG, userHasNoVariationLogMessage); - } else if (entityId === '' || !bucketerParams.variationIdMap.hasOwnProperty(entityId)) { - var invalidVariationIdLogMessage = sprintf(LOG_MESSAGES.INVALID_VARIATION_ID, MODULE_NAME); - bucketerParams.logger.log(LOG_LEVEL.WARNING, invalidVariationIdLogMessage); - return null; - } else { - var variationKey = bucketerParams.variationIdMap[entityId].key; - var userInVariationLogMessage = sprintf(LOG_MESSAGES.USER_HAS_VARIATION, MODULE_NAME, bucketerParams.userId, variationKey, bucketerParams.experimentKey); - bucketerParams.logger.log(LOG_LEVEL.INFO, userInVariationLogMessage); - } - - return entityId; - }, - - /** - * Returns bucketed experiment ID to compare against experiment user is being called into - * @param {Object} group Group that experiment is in - * @param {string} bucketingId Bucketing ID - * @param {string} userId ID of user to be bucketed into experiment - * @param {Object} logger Logger implementation - * @return {string} ID of experiment if user is bucketed into experiment within the group, null otherwise - */ - bucketUserIntoExperiment: function(group, bucketingId, userId, logger) { - var bucketingKey = sprintf('%s%s', bucketingId, group.id); - var bucketValue = module.exports._generateBucketValue(bucketingKey); - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, MODULE_NAME, bucketValue, userId)); - var trafficAllocationConfig = group.trafficAllocation; - var bucketedExperimentId = module.exports._findBucket(bucketValue, trafficAllocationConfig); - return bucketedExperimentId; - }, - - /** - * Returns entity ID associated with bucket value - * @param {string} bucketValue - * @param {Object[]} trafficAllocationConfig - * @param {number} trafficAllocationConfig[].endOfRange - * @param {number} trafficAllocationConfig[].entityId - * @return {string} Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise - */ - _findBucket: function(bucketValue, trafficAllocationConfig) { - for (var i = 0; i < trafficAllocationConfig.length; i++) { - if (bucketValue < trafficAllocationConfig[i].endOfRange) { - return trafficAllocationConfig[i].entityId; - } - } - return null; - }, - - /** - * Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE) - * @param {string} bucketingKey String value for bucketing - * @return {string} the generated bucket value - * @throws If bucketing value is not a valid string - */ - _generateBucketValue: function(bucketingKey) { - try { - // NOTE: the mmh library already does cast the hash value as an unsigned 32bit int - // https://github.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115 - var hashValue = murmurhash.v3(bucketingKey, HASH_SEED); - var ratio = hashValue / MAX_HASH_VALUE; - return parseInt(ratio * MAX_TRAFFIC_VALUE, 10); - } catch (ex) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_BUCKETING_ID, MODULE_NAME, bucketingKey, ex.message)); - } - }, -}; diff --git a/packages/optimizely-sdk/lib/core/bucketer/index.tests.js b/packages/optimizely-sdk/lib/core/bucketer/index.tests.js deleted file mode 100644 index 34dcea933..000000000 --- a/packages/optimizely-sdk/lib/core/bucketer/index.tests.js +++ /dev/null @@ -1,359 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var bucketer = require('./'); -var enums = require('../../utils/enums'); -var logger = require('../../plugins/logger'); -var projectConfig = require('../project_config'); -var sprintf = require('sprintf-js').sprintf; -var testData = require('../../tests/test_data').getTestProjectConfig(); - -var chai = require('chai'); -var assert = chai.assert; -var expect = chai.expect; -var cloneDeep = require('lodash/cloneDeep'); -var sinon = require('sinon'); - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; - -describe('lib/core/bucketer', function() { - describe('APIs', function() { - describe('bucket', function() { - var configObj; - var createdLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - var bucketerParams; - - beforeEach(function() { - sinon.stub(createdLogger, 'log'); - }); - - afterEach(function() { - createdLogger.log.restore(); - }); - - describe('return values for bucketing (excluding groups)', function() { - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData); - bucketerParams = { - experimentId: configObj.experiments[0].id, - experimentKey: configObj.experiments[0].key, - trafficAllocationConfig: configObj.experiments[0].trafficAllocation, - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - }; - sinon.stub(bucketer, '_generateBucketValue') - .onFirstCall().returns(50) - .onSecondCall().returns(50000); - }); - - afterEach(function() { - bucketer._generateBucketValue.restore(); - }); - - it('should return correct variation ID when provided bucket value', function() { - var bucketerParamsTest1 = cloneDeep(bucketerParams); - bucketerParamsTest1.userId = 'ppid1'; - expect(bucketer.bucket(bucketerParamsTest1)).to.equal('111128'); - - var bucketedUser_log1 = createdLogger.log.args[0][1]; - var bucketedUser_log2 = createdLogger.log.args[1][1]; - - expect(bucketedUser_log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_VARIATION_BUCKET, 'BUCKETER', '50', 'ppid1')); - expect(bucketedUser_log2).to.equal(sprintf(LOG_MESSAGES.USER_HAS_VARIATION, 'BUCKETER', 'ppid1', 'control', 'testExperiment')); - - var bucketerParamsTest2 = cloneDeep(bucketerParams); - bucketerParamsTest2.userId = 'ppid2'; - expect(bucketer.bucket(bucketerParamsTest2)).to.equal(null); - - var notBucketedUser_log1 = createdLogger.log.args[2][1]; - var notBucketedUser_log2 = createdLogger.log.args[3][1]; - - expect(notBucketedUser_log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_VARIATION_BUCKET, 'BUCKETER', '50000', 'ppid2')); - expect(notBucketedUser_log2).to.equal(sprintf(LOG_MESSAGES.USER_HAS_NO_VARIATION, 'BUCKETER', 'ppid2', 'testExperiment')); - }); - }); - - describe('return values for bucketing (including groups)', function() { - var bucketerStub; - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData); - bucketerParams = { - experimentId: configObj.experiments[0].id, - experimentKey: configObj.experiments[0].key, - trafficAllocationConfig: configObj.experiments[0].trafficAllocation, - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - }; - bucketerStub = sinon.stub(bucketer, '_generateBucketValue'); - }); - - afterEach(function() { - bucketer._generateBucketValue.restore(); - }); - - describe('random groups', function() { - bucketerParams = {}; - beforeEach(function() { - bucketerParams = { - experimentId: configObj.experiments[4].id, - experimentKey: configObj.experiments[4].key, - trafficAllocationConfig: configObj.experiments[4].trafficAllocation, - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - userId: 'testUser', - }; - }); - - it('should return the proper variation for a user in a grouped experiment', function() { - bucketerStub.onFirstCall().returns(50); - bucketerStub.onSecondCall().returns(50); - - expect(bucketer.bucket(bucketerParams)).to.equal('551'); - - sinon.assert.calledTwice(bucketerStub); - sinon.assert.callCount(createdLogger.log, 4); - - var log1 = createdLogger.log.args[0][1]; - expect(log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 'BUCKETER', '50', 'testUser')); - - var log2 = createdLogger.log.args[1][1]; - expect(log2).to.equal(sprintf(LOG_MESSAGES.USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, 'BUCKETER', 'testUser', 'groupExperiment1', '666')); - - var log3 = createdLogger.log.args[2][1]; - expect(log3).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_VARIATION_BUCKET, 'BUCKETER', '50', 'testUser')); - - var log4 = createdLogger.log.args[3][1]; - expect(log4).to.equal(sprintf(LOG_MESSAGES.USER_HAS_VARIATION, 'BUCKETER', 'testUser', 'var1exp1', 'groupExperiment1')); - }); - - it('should return null when a user is bucketed into a different grouped experiment than the one speicfied', function() { - bucketerStub.returns(5000); - - expect(bucketer.bucket(bucketerParams)).to.equal(null); - - sinon.assert.calledOnce(bucketerStub); - sinon.assert.calledTwice(createdLogger.log); - - var log1 = createdLogger.log.args[0][1]; - expect(log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 'BUCKETER', '5000', 'testUser')); - var log2 = createdLogger.log.args[1][1]; - expect(log2).to.equal(sprintf(LOG_MESSAGES.USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP, 'BUCKETER', 'testUser', 'groupExperiment1', '666')); - }); - - it('should return null when a user is not bucketed into any experiments in the random group', function() { - bucketerStub.returns(50000); - - expect(bucketer.bucket(bucketerParams)).to.equal(null); - - sinon.assert.calledOnce(bucketerStub); - sinon.assert.calledTwice(createdLogger.log); - - var log1 = createdLogger.log.args[0][1]; - expect(log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 'BUCKETER', '50000', 'testUser')); - var log2 = createdLogger.log.args[1][1]; - expect(log2).to.equal(sprintf(LOG_MESSAGES.USER_NOT_IN_ANY_EXPERIMENT, 'BUCKETER', 'testUser', '666')); - }); - - it('should return null when a user is bucketed into traffic space of deleted experiment within a random group', function() { - bucketerStub.returns(9000); - - expect(bucketer.bucket(bucketerParams)).to.equal(null); - - sinon.assert.calledOnce(bucketerStub); - sinon.assert.calledTwice(createdLogger.log); - - var log1 = createdLogger.log.args[0][1]; - expect(log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, 'BUCKETER', '9000', 'testUser')); - var log2 = createdLogger.log.args[1][1]; - expect(log2).to.equal(sprintf(LOG_MESSAGES.USER_NOT_IN_ANY_EXPERIMENT, 'BUCKETER', 'testUser', '666')); - }); - - it('should throw an error if group ID is not in the datafile', function() { - var bucketerParamsWithInvalidGroupId = cloneDeep(bucketerParams); - bucketerParamsWithInvalidGroupId.experimentKeyMap[configObj.experiments[4].key].groupId = '6969'; - - assert.throws(function() { - bucketer.bucket(bucketerParamsWithInvalidGroupId); - }, sprintf(ERROR_MESSAGES.INVALID_GROUP_ID, 'BUCKETER', '6969')); - }); - }); - - describe('overlapping groups', function() { - bucketerParams = {}; - beforeEach(function() { - bucketerParams = { - experimentId: configObj.experiments[6].id, - experimentKey: configObj.experiments[6].key, - trafficAllocationConfig: configObj.experiments[6].trafficAllocation, - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - userId: 'testUser', - }; - }); - - it('should return a variation when a user falls into an experiment within an overlapping group', function() { - bucketerStub.returns(0); - - expect(bucketer.bucket(bucketerParams)).to.equal('553'); - - sinon.assert.calledOnce(bucketerStub); - sinon.assert.calledTwice(createdLogger.log); - - var log1 = createdLogger.log.args[0][1]; - expect(log1).to.equal(sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_VARIATION_BUCKET, 'BUCKETER', '0', 'testUser')); - var log2 = createdLogger.log.args[1][1]; - expect(log2).to.equal(sprintf(LOG_MESSAGES.USER_HAS_VARIATION, 'BUCKETER', 'testUser', 'overlappingvar1', 'overlappingGroupExperiment1')); - }); - - it('should return null when a user does not fall into an experiment within an overlapping group', function() { - bucketerStub.returns(3000); - - expect(bucketer.bucket(bucketerParams)).to.equal(null); - }); - }); - }); - - describe('when the bucket value falls into empty traffic allocation ranges', function() { - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData); - bucketerParams = { - experimentId: configObj.experiments[0].id, - experimentKey: configObj.experiments[0].key, - trafficAllocationConfig: [{ - entityId: '', - endOfRange: 5000 - }, { - entityId: '', - endOfRange: 10000 - }], - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - }; - }); - - it('should return null', function() { - var bucketerParamsTest1 = cloneDeep(bucketerParams); - bucketerParamsTest1.userId = 'ppid1'; - expect(bucketer.bucket(bucketerParamsTest1)).to.equal(null); - }); - }); - - describe('when the traffic allocation has invalid variation ids', function() { - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData); - bucketerParams = { - experimentId: configObj.experiments[0].id, - experimentKey: configObj.experiments[0].key, - trafficAllocationConfig: [{ - entityId: -1, - endOfRange: 5000 - }, { - entityId: -2, - endOfRange: 10000 - }], - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - }; - }); - - it('should return null', function() { - var bucketerParamsTest1 = cloneDeep(bucketerParams); - bucketerParamsTest1.userId = 'ppid1'; - expect(bucketer.bucket(bucketerParamsTest1)).to.equal(null); - }); - }); - }); - - describe('_generateBucketValue', function() { - it('should return a bucket value for different inputs', function() { - var experimentId = 1886780721; - var bucketingKey1 = sprintf('%s%s', 'ppid1', experimentId); - var bucketingKey2 = sprintf('%s%s', 'ppid2', experimentId); - var bucketingKey3 = sprintf('%s%s', 'ppid2', 1886780722); - var bucketingKey4 = sprintf('%s%s', 'ppid3', experimentId); - - expect(bucketer._generateBucketValue(bucketingKey1)).to.equal(5254); - expect(bucketer._generateBucketValue(bucketingKey2)).to.equal(4299); - expect(bucketer._generateBucketValue(bucketingKey3)).to.equal(2434); - expect(bucketer._generateBucketValue(bucketingKey4)).to.equal(5439); - }); - - it('should return an error if it cannot generate the hash value', function() { - assert.throws(function() { - bucketer._generateBucketValue(null); - }, sprintf(ERROR_MESSAGES.INVALID_BUCKETING_ID, 'BUCKETER', null, 'Cannot read property \'length\' of null')); - }); - }); - - describe('testBucketWithBucketingId', function() { - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData); - bucketerParams = { - trafficAllocationConfig: configObj.experiments[0].trafficAllocation, - variationIdMap: configObj.variationIdMap, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - logger: createdLogger, - }; - }); - - it('check that a non null bucketingId buckets a variation different than the one expected with userId', function () { - var bucketerParams1 = cloneDeep(bucketerParams); - bucketerParams1['userId'] = 'testBucketingIdControl'; - bucketerParams1['bucketingId'] = '123456789'; - bucketerParams1['experimentKey'] = 'testExperiment'; - bucketerParams1['experimentId'] = '111127'; - expect(bucketer.bucket(bucketerParams1)).to.equal('111129'); - }); - - it('check that a null bucketing ID defaults to bucketing with the userId', function () { - var bucketerParams2 = cloneDeep(bucketerParams); - bucketerParams2['userId'] = 'testBucketingIdControl'; - bucketerParams2['bucketingId'] = null; - bucketerParams2['experimentKey'] = 'testExperiment'; - bucketerParams2['experimentId'] = '111127'; - expect(bucketer.bucket(bucketerParams2)).to.equal('111128'); - }); - - it('check that bucketing works with an experiment in group', function () { - var bucketerParams4 = cloneDeep(bucketerParams); - bucketerParams4['userId'] = 'testBucketingIdControl'; - bucketerParams4['bucketingId'] = '123456789'; - bucketerParams4['experimentKey'] = 'groupExperiment2'; - bucketerParams4['experimentId'] = '443'; - expect(bucketer.bucket(bucketerParams4)).to.equal('111128'); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.js b/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.js deleted file mode 100644 index 2f5a11c2b..000000000 --- a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.js +++ /dev/null @@ -1,125 +0,0 @@ -/**************************************************************************** - * Copyright 2018, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var AND_CONDITION = 'and'; -var OR_CONDITION = 'or'; -var NOT_CONDITION = 'not'; - -var DEFAULT_OPERATOR_TYPES = [AND_CONDITION, OR_CONDITION, NOT_CONDITION]; - -/** - * Top level method to evaluate conditions - * @param {Array|*} conditions Nested array of and/or conditions, or a single leaf - * condition value of any type - * Example: ['and', '0', ['or', '1', '2']] - * @param {Function} leafEvaluator Function which will be called to evaluate leaf condition - * values - * @return {?Boolean} Result of evaluating the conditions using the operator - * rules and the leaf evaluator. A return value of null - * indicates that the conditions are invalid or unable to be - * evaluated - */ -function evaluate(conditions, leafEvaluator) { - if (Array.isArray(conditions)) { - var firstOperator = conditions[0]; - var restOfConditions = conditions.slice(1); - - if (DEFAULT_OPERATOR_TYPES.indexOf(firstOperator) === -1) { - // Operator to apply is not explicit - assume 'or' - firstOperator = OR_CONDITION; - restOfConditions = conditions; - } - - switch (firstOperator) { - case AND_CONDITION: - return andEvaluator(restOfConditions, leafEvaluator); - case NOT_CONDITION: - return notEvaluator(restOfConditions, leafEvaluator); - default: // firstOperator is OR_CONDITION - return orEvaluator(restOfConditions, leafEvaluator); - } - } - - var leafCondition = conditions; - return leafEvaluator(leafCondition); -} - -/** - * Evaluates an array of conditions as if the evaluator had been applied - * to each entry and the results AND-ed together. - * @param {Array} conditions Array of conditions ex: [operand_1, operand_2] - * @param {Function} leafEvaluator Function which will be called to evaluate leaf condition values - * @return {?Boolean} Result of evaluating the conditions. A return value of null - * indicates that the conditions are invalid or unable to be - * evaluated. - */ -function andEvaluator(conditions, leafEvaluator) { - var sawNullResult = false; - for (var i = 0; i < conditions.length; i++) { - var conditionResult = evaluate(conditions[i], leafEvaluator); - if (conditionResult === false) { - return false; - } - if (conditionResult === null) { - sawNullResult = true; - } - } - return sawNullResult ? null : true; -} - -/** - * Evaluates an array of conditions as if the evaluator had been applied - * to a single entry and NOT was applied to the result. - * @param {Array} conditions Array of conditions ex: [operand_1] - * @param {Function} leafEvaluator Function which will be called to evaluate leaf condition values - * @return {?Boolean} Result of evaluating the conditions. A return value of null - * indicates that the conditions are invalid or unable to be - * evaluated. - */ -function notEvaluator(conditions, leafEvaluator) { - if (conditions.length > 0) { - var result = evaluate(conditions[0], leafEvaluator); - return result === null ? null : !result; - } - return null; -} - -/** - * Evaluates an array of conditions as if the evaluator had been applied - * to each entry and the results OR-ed together. - * @param {Array} conditions Array of conditions ex: [operand_1, operand_2] - * @param {Function} leafEvaluator Function which will be called to evaluate leaf condition values - * @return {?Boolean} Result of evaluating the conditions. A return value of null - * indicates that the conditions are invalid or unable to be - * evaluated. - */ -function orEvaluator(conditions, leafEvaluator) { - var sawNullResult = false; - for (var i = 0; i < conditions.length; i++) { - var conditionResult = evaluate(conditions[i], leafEvaluator); - if (conditionResult === true) { - return true; - } - if (conditionResult === null) { - sawNullResult = true; - } - } - return sawNullResult ? null : false; -} - -module.exports = { - evaluate: evaluate, -}; diff --git a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js deleted file mode 100644 index fbca692d4..000000000 --- a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js +++ /dev/null @@ -1,259 +0,0 @@ -/**************************************************************************** - * Copyright 2018-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var fns = require('../../utils/fns'); -var enums = require('../../utils/enums'); -var sprintf = require('sprintf-js').sprintf; - -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MODULE_NAME = 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR'; - -var CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute'; - -var EXACT_MATCH_TYPE = 'exact'; -var EXISTS_MATCH_TYPE = 'exists'; -var GREATER_THAN_MATCH_TYPE = 'gt'; -var LESS_THAN_MATCH_TYPE = 'lt'; -var SUBSTRING_MATCH_TYPE = 'substring'; - -var MATCH_TYPES = [ - EXACT_MATCH_TYPE, - EXISTS_MATCH_TYPE, - GREATER_THAN_MATCH_TYPE, - LESS_THAN_MATCH_TYPE, - SUBSTRING_MATCH_TYPE, -]; - -var EVALUATORS_BY_MATCH_TYPE = {}; -EVALUATORS_BY_MATCH_TYPE[EXACT_MATCH_TYPE] = exactEvaluator; -EVALUATORS_BY_MATCH_TYPE[EXISTS_MATCH_TYPE] = existsEvaluator; -EVALUATORS_BY_MATCH_TYPE[GREATER_THAN_MATCH_TYPE] = greaterThanEvaluator; -EVALUATORS_BY_MATCH_TYPE[LESS_THAN_MATCH_TYPE] = lessThanEvaluator; -EVALUATORS_BY_MATCH_TYPE[SUBSTRING_MATCH_TYPE] = substringEvaluator; - -/** - * Given a custom attribute audience condition and user attributes, evaluate the - * condition against the attributes. - * @param {Object} condition - * @param {Object} userAttributes - * @param {Object} logger - * @return {?Boolean} true/false if the given user attributes match/don't match the given condition, - * null if the given user attributes and condition can't be evaluated - */ -function evaluate(condition, userAttributes, logger) { - if (condition.type !== CUSTOM_ATTRIBUTE_CONDITION_TYPE) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition))); - return null; - } - - var conditionMatch = condition.match; - if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_MATCH_TYPE, MODULE_NAME, JSON.stringify(condition))); - return null; - } - - var attributeKey = condition.name; - if (!userAttributes.hasOwnProperty(attributeKey) && conditionMatch != EXISTS_MATCH_TYPE) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.MISSING_ATTRIBUTE_VALUE, MODULE_NAME, JSON.stringify(condition), attributeKey)); - return null; - } - - var evaluatorForMatch = EVALUATORS_BY_MATCH_TYPE[conditionMatch] || exactEvaluator; - return evaluatorForMatch(condition, userAttributes, logger); -} - -/** - * Returns true if the value is valid for exact conditions. Valid values include - * strings, booleans, and numbers that aren't NaN, -Infinity, or Infinity. - * @param value - * @returns {Boolean} - */ -function isValueTypeValidForExactConditions(value) { - return typeof value === 'string' || typeof value === 'boolean' || - fns.isNumber(value); -} - -/** - * Evaluate the given exact match condition for the given user attributes - * @param {Object} condition - * @param {Object} userAttributes - * @param {Object} logger - * @return {?Boolean} true if the user attribute value is equal (===) to the condition value, - * false if the user attribute value is not equal (!==) to the condition value, - * null if the condition value or user attribute value has an invalid type, or - * if there is a mismatch between the user attribute type and the condition value - * type - */ -function exactEvaluator(condition, userAttributes, logger) { - var conditionValue = condition.value; - var conditionValueType = typeof conditionValue; - var conditionName = condition.name; - var userValue = userAttributes[conditionName]; - var userValueType = typeof userValue; - - if (!isValueTypeValidForExactConditions(conditionValue) || (fns.isNumber(conditionValue) && !fns.isFinite(conditionValue))) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_CONDITION_VALUE, MODULE_NAME, JSON.stringify(condition))); - return null; - } - - if (userValue === null) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE_NULL, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - if (!isValueTypeValidForExactConditions(userValue) || conditionValueType !== userValueType) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE, MODULE_NAME, JSON.stringify(condition), userValueType, conditionName)); - return null; - } - - if (fns.isNumber(userValue) && !fns.isFinite(userValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.OUT_OF_BOUNDS, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - return conditionValue === userValue; -} - -/** - * Evaluate the given exists match condition for the given user attributes - * @param {Object} condition - * @param {Object} userAttributes - * @returns {Boolean} true if both: - * 1) the user attributes have a value for the given condition, and - * 2) the user attribute value is neither null nor undefined - * Returns false otherwise - */ -function existsEvaluator(condition, userAttributes) { - var userValue = userAttributes[condition.name]; - return typeof userValue !== 'undefined' && userValue !== null; -} - -/** - * Evaluate the given greater than match condition for the given user attributes - * @param {Object} condition - * @param {Object} userAttributes - * @param {Object} logger - * @returns {?Boolean} true if the user attribute value is greater than the condition value, - * false if the user attribute value is less than or equal to the condition value, - * null if the condition value isn't a number or the user attribute value - * isn't a number - */ -function greaterThanEvaluator(condition, userAttributes, logger) { - var conditionName = condition.name; - var userValue = userAttributes[conditionName]; - var userValueType = typeof userValue; - var conditionValue = condition.value; - - if (!fns.isFinite(conditionValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_CONDITION_VALUE, MODULE_NAME, JSON.stringify(condition))); - return null; - } - - if (userValue === null) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE_NULL, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - if (!fns.isNumber(userValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE, MODULE_NAME, JSON.stringify(condition), userValueType, conditionName)); - return null; - } - - if (!fns.isFinite(userValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.OUT_OF_BOUNDS, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - return userValue > conditionValue; -} - -/** - * Evaluate the given less than match condition for the given user attributes - * @param {Object} condition - * @param {Object} userAttributes - * @param {Object} logger - * @returns {?Boolean} true if the user attribute value is less than the condition value, - * false if the user attribute value is greater than or equal to the condition value, - * null if the condition value isn't a number or the user attribute value isn't a - * number - */ -function lessThanEvaluator(condition, userAttributes, logger) { - var conditionName = condition.name; - var userValue = userAttributes[condition.name]; - var userValueType = typeof userValue; - var conditionValue = condition.value; - - if (!fns.isFinite(conditionValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_CONDITION_VALUE, MODULE_NAME, JSON.stringify(condition))); - return null; - } - - if (userValue === null) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE_NULL, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - if (!fns.isNumber(userValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE, MODULE_NAME, JSON.stringify(condition), userValueType, conditionName)); - return null; - } - - if (!fns.isFinite(userValue)) { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.OUT_OF_BOUNDS, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - return userValue < conditionValue; -} - -/** - * Evaluate the given substring match condition for the given user attributes - * @param {Object} condition - * @param {Object} userAttributes - * @param {Object} logger - * @returns {?Boolean} true if the condition value is a substring of the user attribute value, - * false if the condition value is not a substring of the user attribute value, - * null if the condition value isn't a string or the user attribute value - * isn't a string - */ -function substringEvaluator(condition, userAttributes, logger) { - var conditionName = condition.name; - var userValue = userAttributes[condition.name]; - var userValueType = typeof userValue; - var conditionValue = condition.value; - - if (typeof conditionValue !== 'string') { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_CONDITION_VALUE, MODULE_NAME, JSON.stringify(condition))); - return null; - } - - if (userValue === null) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE_NULL, MODULE_NAME, JSON.stringify(condition), conditionName)); - return null; - } - - if (typeof userValue !== 'string') { - logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNEXPECTED_TYPE, MODULE_NAME, JSON.stringify(condition), userValueType, conditionName)); - return null; - } - - return userValue.indexOf(conditionValue) !== -1; -} - -module.exports = { - evaluate: evaluate -}; diff --git a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js deleted file mode 100644 index 9a1e7f4ef..000000000 --- a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js +++ /dev/null @@ -1,588 +0,0 @@ -/**************************************************************************** - * Copyright 2018-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var customAttributeEvaluator = require('./'); -var enums = require('../../utils/enums'); -var LOG_LEVEL = enums.LOG_LEVEL; -var logger = require('../../plugins/logger'); - -var chai = require('chai'); -var sinon = require('sinon'); -var assert = chai.assert; - -var browserConditionSafari = { - name: 'browser_type', - value: 'safari', - type: 'custom_attribute', -}; -var booleanCondition = { - name: 'is_firefox', - value: true, - type: 'custom_attribute', -}; -var integerCondition = { - name: 'num_users', - value: 10, - type: 'custom_attribute', -}; -var doubleCondition = { - name: 'pi_value', - value: 3.14, - type: 'custom_attribute', -}; - -describe('lib/core/custom_attribute_condition_evaluator', function() { - var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - - beforeEach(function () { - sinon.stub(mockLogger, 'log'); - }); - - afterEach(function () { - mockLogger.log.restore(); - }); - - it('should return true when the attributes pass the audience conditions and no match type is provided', function() { - var userAttributes = { - browser_type: 'safari', - }; - - assert.isTrue(customAttributeEvaluator.evaluate(browserConditionSafari, userAttributes, mockLogger)); - }); - - it('should return false when the attributes do not pass the audience conditions and no match type is provided', function() { - var userAttributes = { - browser_type: 'firefox', - }; - - assert.isFalse(customAttributeEvaluator.evaluate(browserConditionSafari, userAttributes, mockLogger)); - }); - - it('should evaluate different typed attributes', function() { - var userAttributes = { - browser_type: 'safari', - is_firefox: true, - num_users: 10, - pi_value: 3.14, - }; - - assert.isTrue(customAttributeEvaluator.evaluate(browserConditionSafari, userAttributes, mockLogger)); - assert.isTrue(customAttributeEvaluator.evaluate(booleanCondition, userAttributes, mockLogger)); - assert.isTrue(customAttributeEvaluator.evaluate(integerCondition, userAttributes, mockLogger)); - assert.isTrue(customAttributeEvaluator.evaluate(doubleCondition, userAttributes, mockLogger)); - }); - - it('should log and return null when condition has an invalid type property', function() { - var result = customAttributeEvaluator.evaluate( - { match: 'exact', name: 'weird_condition', type: 'weird', value: 'hi' }, - { weird_condition: 'bye' }, - mockLogger - ); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"weird_condition","type":"weird","value":"hi"} has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.'); - }); - - it('should log and return null when condition has no type property', function() { - var result = customAttributeEvaluator.evaluate( - { match: 'exact', name: 'weird_condition', value: 'hi' }, - { weird_condition: 'bye' }, - mockLogger - ); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"weird_condition","value":"hi"} has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.'); - }); - - it('should log and return null when condition has an invalid match property', function() { - var result = customAttributeEvaluator.evaluate( - { match: 'weird', name: 'weird_condition', type: 'custom_attribute', value: 'hi' }, - { weird_condition: 'bye' }, - mockLogger - ); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"weird","name":"weird_condition","type":"custom_attribute","value":"hi"} uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK.'); - }); - - describe('exists match type', function() { - var existsCondition = { - match: 'exists', - name: 'input_value', - type: 'custom_attribute', - }; - - it('should return false if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, {}, mockLogger); - assert.isFalse(result); - sinon.assert.notCalled(mockLogger.log); - }); - - it('should return false if the user-provided value is undefined', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: undefined }, mockLogger); - assert.isFalse(result); - }); - - it('should return false if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: null }, mockLogger); - assert.isFalse(result); - }); - - it('should return true if the user-provided value is a string', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: 'hi' }, mockLogger); - assert.isTrue(result); - }); - - it('should return true if the user-provided value is a number', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: 10 }, mockLogger); - assert.isTrue(result); - }); - - it('should return true if the user-provided value is a boolean', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: true }, mockLogger); - assert.isTrue(result); - }); - }); - - describe('exact match type', function() { - describe('with a string condition value', function() { - var exactStringCondition = { - match: 'exact', - name: 'favorite_constellation', - type: 'custom_attribute', - value: 'Lacerta', - }; - - it('should return true if the user-provided value is equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, { favorite_constellation: 'Lacerta' }, mockLogger); - assert.isTrue(result); - }); - - it('should return false if the user-provided value is not equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, { favorite_constellation: 'The Big Dipper' }, mockLogger); - assert.isFalse(result); - }); - - it('should log and return null if condition value is of an unexpected type', function() { - var invalidExactCondition = { - match: 'exact', - name: 'favorite_constellation', - type: 'custom_attribute', - value: [], - }; - var result = customAttributeEvaluator.evaluate(invalidExactCondition, { favorite_constellation: 'Lacerta' }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"favorite_constellation","type":"custom_attribute","value":[]} evaluated to UNKNOWN because the condition value is not supported.'); - }); - - it('should log and return null if the user-provided value is of a different type than the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, { favorite_constellation: false }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"favorite_constellation","type":"custom_attribute","value":"Lacerta"} evaluated to UNKNOWN because a value of type "boolean" was passed for user attribute "favorite_constellation".'); - }); - - it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, { favorite_constellation: null }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"favorite_constellation","type":"custom_attribute","value":"Lacerta"} evaluated to UNKNOWN because a null value was passed for user attribute "favorite_constellation".'); - }); - - it('should log and return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, {}, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"favorite_constellation","type":"custom_attribute","value":"Lacerta"} evaluated to UNKNOWN because no value was passed for user attribute "favorite_constellation".'); - }); - - it('should log and return null if the user-provided value is of an unexpected type', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, { favorite_constellation: [] }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"favorite_constellation","type":"custom_attribute","value":"Lacerta"} evaluated to UNKNOWN because a value of type "object" was passed for user attribute "favorite_constellation".'); - }); - }); - - describe('with a number condition value', function() { - var exactNumberCondition = { - match: 'exact', - name: 'lasers_count', - type: 'custom_attribute', - value: 9000, - }; - - it('should return true if the user-provided value is equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: 9000 }, mockLogger); - assert.isTrue(result); - }); - - it('should return false if the user-provided value is not equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: 8000 }, mockLogger); - assert.isFalse(result); - }); - - it('should log and return null if the user-provided value is of a different type than the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: 'yes' }, mockLogger); - assert.isNull(result); - - result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: '1000' }, mockLogger); - assert.isNull(result); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[0][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"lasers_count","type":"custom_attribute","value":9000} evaluated to UNKNOWN because a value of type "string" was passed for user attribute "lasers_count".'); - assert.strictEqual(mockLogger.log.args[1][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[1][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"lasers_count","type":"custom_attribute","value":9000} evaluated to UNKNOWN because a value of type "string" was passed for user attribute "lasers_count".'); - }); - - it('should log and return null if the user-provided number value is out of bounds', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: -Infinity }, mockLogger); - assert.isNull(result); - - result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: -Math.pow(2, 53) - 2 }, mockLogger); - assert.isNull(result); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[0][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"lasers_count","type":"custom_attribute","value":9000} evaluated to UNKNOWN because the number value for user attribute "lasers_count" is not in the range [-2^53, +2^53].'); - assert.strictEqual(mockLogger.log.args[1][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[1][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"lasers_count","type":"custom_attribute","value":9000} evaluated to UNKNOWN because the number value for user attribute "lasers_count" is not in the range [-2^53, +2^53].'); - }); - - it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, {}, mockLogger); - assert.isNull(result); - }); - - it('should return null if the condition value is not finite', function() { - var invalidValueCondition = { - match: 'exact', - name: 'lasers_count', - type: 'custom_attribute', - value: Infinity, - }; - var result = customAttributeEvaluator.evaluate(invalidValueCondition, { lasers_count: 9000 }, mockLogger); - assert.isNull(result); - - invalidValueCondition.value = Math.pow(2, 53) + 2; - result = customAttributeEvaluator.evaluate(invalidValueCondition, { lasers_count: 9000 }, mockLogger); - assert.isNull(result); - }); - }); - - describe('with a boolean condition value', function() { - var exactBoolCondition = { - match: 'exact', - name: 'did_register_user', - type: 'custom_attribute', - value: false, - }; - - it('should return true if the user-provided value is equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, { did_register_user: false }, mockLogger); - assert.isTrue(result); - }); - - it('should return false if the user-provided value is not equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, { did_register_user: true }, mockLogger); - assert.isFalse(result); - }); - - it('should return null if the user-provided value is of a different type than the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, { did_register_user: 10 }, mockLogger); - assert.isNull(result); - }); - - it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, {}, mockLogger); - assert.isNull(result); - }); - }); - }); - - describe('substring match type', function() { - var substringCondition = { - match: 'substring', - name: 'headline_text', - type: 'custom_attribute', - value: 'buy now', - }; - - it('should return true if the condition value is a substring of the user-provided value', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, { - headline_text: 'Limited time, buy now!', - }, mockLogger); - assert.isTrue(result); - }); - - it('should return false if the user-provided value is not a substring of the condition value', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, { - headline_text: 'Breaking news!', - }, mockLogger); - assert.isFalse(result); - }); - - it('should log and return null if the user-provided value is not a string', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, { - headline_text: 10, - }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"substring","name":"headline_text","type":"custom_attribute","value":"buy now"} evaluated to UNKNOWN because a value of type "number" was passed for user attribute "headline_text".'); - }); - - it('should log and return null if the condition value is not a string', function() { - var nonStringCondition = { - match: 'substring', - name: 'headline_text', - type: 'custom_attribute', - value: 10, - }; - - var result = customAttributeEvaluator.evaluate(nonStringCondition, {headline_text: 'hello'}, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"substring","name":"headline_text","type":"custom_attribute","value":10} evaluated to UNKNOWN because the condition value is not supported.'); - }); - - it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, { headline_text: null }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"substring","name":"headline_text","type":"custom_attribute","value":"buy now"} evaluated to UNKNOWN because a null value was passed for user attribute "headline_text".'); - }); - - it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, {}, mockLogger); - assert.isNull(result); - }); - }); - - describe('greater than match type', function() { - var gtCondition = { - match: 'gt', - name: 'meters_travelled', - type: 'custom_attribute', - value: 48.2, - }; - - it('should return true if the user-provided value is greater than the condition value', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, { - meters_travelled: 58.4, - }, mockLogger); - assert.isTrue(result); - }); - - it('should return false if the user-provided value is not greater than the condition value', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, { - meters_travelled: 20, - }, mockLogger); - assert.isFalse(result); - }); - - it('should log and return null if the user-provided value is not a number', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, { - meters_travelled: 'a long way', - }, mockLogger); - assert.isNull(result); - - result = customAttributeEvaluator.evaluate(gtCondition, { - meters_travelled: '1000', - }, mockLogger); - assert.isNull(result); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"gt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because a value of type "string" was passed for user attribute "meters_travelled".'); - assert.strictEqual(mockLogger.log.args[1][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"gt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because a value of type "string" was passed for user attribute "meters_travelled".'); - }); - - it('should log and return null if the user-provided number value is out of bounds', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, { - meters_travelled: -Infinity, - }, mockLogger); - assert.isNull(result); - - result = customAttributeEvaluator.evaluate(gtCondition, { - meters_travelled: Math.pow(2, 53) + 2, - }, mockLogger); - assert.isNull(result); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[0][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"gt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not in the range [-2^53, +2^53].'); - assert.strictEqual(mockLogger.log.args[1][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[1][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"gt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not in the range [-2^53, +2^53].'); - }); - - it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, { meters_travelled: null }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"gt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because a null value was passed for user attribute "meters_travelled".'); - }); - - it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, {}, mockLogger); - assert.isNull(result); - }); - - it('should return null if the condition value is not a finite number', function() { - var userAttributes = { meters_travelled: 58.4 }; - var invalidValueCondition = { - match: 'gt', - name: 'meters_travelled', - type: 'custom_attribute', - value: Infinity, - }; - var result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes, mockLogger); - assert.isNull(result); - - invalidValueCondition.value = null; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes, mockLogger); - assert.isNull(result); - - invalidValueCondition.value = Math.pow(2, 53) + 2; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes, mockLogger); - assert.isNull(result); - - sinon.assert.calledThrice(mockLogger.log); - var logMessage = mockLogger.log.args[2][1]; - assert.strictEqual(logMessage, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"gt","name":"meters_travelled","type":"custom_attribute","value":9007199254740994} evaluated to UNKNOWN because the condition value is not supported.'); - }); - }); - - describe('less than match type', function() { - var ltCondition = { - match: 'lt', - name: 'meters_travelled', - type: 'custom_attribute', - value: 48.2, - }; - - it('should return true if the user-provided value is less than the condition value', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, { - meters_travelled: 10, - }, mockLogger); - assert.isTrue(result); - }); - - it('should return false if the user-provided value is not less than the condition value', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, { - meters_travelled: 64.64, - }, mockLogger); - assert.isFalse(result); - }); - - it('should log and return null if the user-provided value is not a number', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, { - meters_travelled: true, - }, mockLogger); - assert.isNull(result); - - result = customAttributeEvaluator.evaluate(ltCondition, { - meters_travelled: '48.2', - }, mockLogger); - assert.isNull(result); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because a value of type "boolean" was passed for user attribute "meters_travelled".'); - assert.strictEqual(mockLogger.log.args[1][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because a value of type "string" was passed for user attribute "meters_travelled".'); - }); - - it('should log and return null if the user-provided number value is out of bounds', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, { - meters_travelled: Infinity, - }, mockLogger); - assert.isNull(result); - - result = customAttributeEvaluator.evaluate(ltCondition, { - meters_travelled: Math.pow(2, 53) + 2, - }, mockLogger); - assert.isNull(result); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[0][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not in the range [-2^53, +2^53].'); - assert.strictEqual(mockLogger.log.args[1][0], LOG_LEVEL.WARNING); - assert.strictEqual(mockLogger.log.args[1][1], - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not in the range [-2^53, +2^53].'); - }); - - it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, { meters_travelled: null }, mockLogger); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":48.2} evaluated to UNKNOWN because a null value was passed for user attribute "meters_travelled".'); - }); - - it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, {}, mockLogger); - assert.isNull(result); - }); - - it('should return null if the condition value is not a finite number', function() { - var userAttributes = { meters_travelled: 10 }; - var invalidValueCondition = { - match: 'lt', - name: 'meters_travelled', - type: 'custom_attribute', - value: Infinity, - }; - var result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes, mockLogger); - assert.isNull(result); - - invalidValueCondition.value = {}; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes, mockLogger); - assert.isNull(result); - - invalidValueCondition.value = Math.pow(2, 53) + 2; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes, mockLogger); - assert.isNull(result); - - sinon.assert.calledThrice(mockLogger.log); - var logMessage = mockLogger.log.args[2][1]; - assert.strictEqual(logMessage, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":9007199254740994} evaluated to UNKNOWN because the condition value is not supported.'); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js deleted file mode 100644 index 726588668..000000000 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ /dev/null @@ -1,484 +0,0 @@ -/**************************************************************************** - * Copyright 2017-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var audienceEvaluator = require('../audience_evaluator'); -var bucketer = require('../bucketer'); -var enums = require('../../utils/enums'); -var fns = require('../../utils/fns'); -var projectConfig = require('../project_config'); - -var sprintf = require('sprintf-js').sprintf; - -var MODULE_NAME = 'DECISION_SERVICE'; -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var DECISION_SOURCES = enums.DECISION_SOURCES; - - - -/** - * Optimizely's decision service that determines which variation of an experiment the user will be allocated to. - * - * The decision service contains all logic around how a user decision is made. This includes all of the following (in order): - * 1. Checking experiment status - * 2. Checking forced bucketing - * 3. Checking whitelisting - * 4. Checking user profile service for past bucketing decisions (sticky bucketing) - * 5. Checking audience targeting - * 6. Using Murmurhash3 to bucket the user. - * - * @constructor - * @param {Object} options - * @param {Object} options.configObj The parsed project configuration object that contains all the experiment configurations. - * @param {Object} options.userProfileService An instance of the user profile service for sticky bucketing. - * @param {Object} options.logger An instance of a logger to log messages with. - * @returns {Object} - */ -function DecisionService(options) { - this.configObj = options.configObj; - this.userProfileService = options.userProfileService || null; - this.logger = options.logger; -} - -/** - * Gets variation where visitor will be bucketed. - * @param {string} experimentKey - * @param {string} userId - * @param {Object} attributes - * @return {string|null} the variation the user is bucketed into. - */ -DecisionService.prototype.getVariation = function(experimentKey, userId, attributes) { - // by default, the bucketing ID should be the user ID - var bucketingId = this._getBucketingId(userId, attributes); - - if (!this.__checkIfExperimentIsActive(experimentKey, userId)) { - return null; - } - var experiment = this.configObj.experimentKeyMap[experimentKey]; - var forcedVariationKey = projectConfig.getForcedVariation(this.configObj, experimentKey, userId, this.logger); - if (!!forcedVariationKey) { - return forcedVariationKey; - } - - var variation = this.__getWhitelistedVariation(experiment, userId); - if (!!variation) { - return variation.key; - } - - // check for sticky bucketing - var experimentBucketMap = this.__resolveExperimentBucketMap(userId, attributes); - variation = this.__getStoredVariation(experiment, userId, experimentBucketMap); - if (!!variation) { - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.RETURNING_STORED_VARIATION, MODULE_NAME, variation.key, experimentKey, userId)); - return variation.key; - } - - // Perform regular targeting and bucketing - if (!this.__checkIfUserIsInAudience(experimentKey, userId, attributes)) { - return null; - } - - var bucketerParams = this.__buildBucketerParams(experimentKey, bucketingId, userId); - var variationId = bucketer.bucket(bucketerParams); - variation = this.configObj.variationIdMap[variationId]; - if (!variation) { - return null; - } - - // persist bucketing - this.__saveUserProfile(experiment, variation, userId, experimentBucketMap); - - return variation.key; -}; - -/** - * Merges attributes from attributes[STICKY_BUCKETING_KEY] and userProfileService - * @param {Object} attributes - * @return {Object} finalized copy of experiment_bucket_map - */ -DecisionService.prototype.__resolveExperimentBucketMap = function(userId, attributes) { - attributes = attributes || {} - var userProfile = this.__getUserProfile(userId) || {}; - var attributeExperimentBucketMap = attributes[enums.CONTROL_ATTRIBUTES.STICKY_BUCKETING_KEY]; - return fns.assignIn({}, userProfile.experiment_bucket_map, attributeExperimentBucketMap); -}; - - -/** - * Checks whether the experiment is running or launched - * @param {string} experimentKey Key of experiment being validated - * @param {string} userId ID of user - * @return {boolean} True if experiment is running - */ -DecisionService.prototype.__checkIfExperimentIsActive = function(experimentKey, userId) { - if (!projectConfig.isActive(this.configObj, experimentKey)) { - var experimentNotRunningLogMessage = sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, MODULE_NAME, experimentKey); - this.logger.log(LOG_LEVEL.INFO, experimentNotRunningLogMessage); - return false; - } - - return true; -}; - -/** - * Checks if user is whitelisted into any variation and return that variation if so - * @param {Object} experiment - * @param {string} userId - * @return {string|null} Forced variation if it exists for user ID, otherwise null - */ -DecisionService.prototype.__getWhitelistedVariation = function(experiment, userId) { - if (!fns.isEmpty(experiment.forcedVariations) && experiment.forcedVariations.hasOwnProperty(userId)) { - var forcedVariationKey = experiment.forcedVariations[userId]; - if (experiment.variationKeyMap.hasOwnProperty(forcedVariationKey)) { - var forcedBucketingSucceededMessageLog = sprintf(LOG_MESSAGES.USER_FORCED_IN_VARIATION, MODULE_NAME, userId, forcedVariationKey); - this.logger.log(LOG_LEVEL.INFO, forcedBucketingSucceededMessageLog); - return experiment.variationKeyMap[forcedVariationKey]; - } else { - var forcedBucketingFailedMessageLog = sprintf(LOG_MESSAGES.FORCED_BUCKETING_FAILED, MODULE_NAME, forcedVariationKey, userId); - this.logger.log(LOG_LEVEL.ERROR, forcedBucketingFailedMessageLog); - return null; - } - } - - return null; -}; - -/** - * Checks whether the user is included in experiment audience - * @param {string} experimentKey Key of experiment being validated - * @param {string} userId ID of user - * @param {Object} attributes Optional parameter for user's attributes - * @return {boolean} True if user meets audience conditions - */ -DecisionService.prototype.__checkIfUserIsInAudience = function(experimentKey, userId, attributes) { - var experimentAudienceConditions = projectConfig.getExperimentAudienceConditions(this.configObj, experimentKey); - var audiencesById = projectConfig.getAudiencesById(this.configObj); - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCES_COMBINED, MODULE_NAME, experimentKey, JSON.stringify(experimentAudienceConditions))); - var result = audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, attributes, this.logger); - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT_COMBINED, MODULE_NAME, experimentKey, result.toString().toUpperCase())); - - if (!result) { - var userDoesNotMeetConditionsLogMessage = sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, MODULE_NAME, userId, experimentKey); - this.logger.log(LOG_LEVEL.INFO, userDoesNotMeetConditionsLogMessage); - return false; - } - - return true; -}; - -/** - * Given an experiment key and user ID, returns params used in bucketer call - * @param experimentKey Experiment key used for bucketer - * @param bucketingId ID to bucket user into - * @param userId ID of user to be bucketed - * @return {Object} - */ -DecisionService.prototype.__buildBucketerParams = function(experimentKey, bucketingId, userId) { - var bucketerParams = {}; - bucketerParams.experimentKey = experimentKey; - bucketerParams.experimentId = projectConfig.getExperimentId(this.configObj, experimentKey); - bucketerParams.userId = userId; - bucketerParams.trafficAllocationConfig = projectConfig.getTrafficAllocation(this.configObj, experimentKey); - bucketerParams.experimentKeyMap = this.configObj.experimentKeyMap; - bucketerParams.groupIdMap = this.configObj.groupIdMap; - bucketerParams.variationIdMap = this.configObj.variationIdMap; - bucketerParams.logger = this.logger; - bucketerParams.bucketingId = bucketingId; - return bucketerParams; -}; - -/** - * Pull the stored variation out of the experimentBucketMap for an experiment/userId - * @param {Object} experiment - * @param {String} userId - * @param {Object} experimentBucketMap mapping experiment => { variation_id: <variationId> } - * @return {Object} the stored variation or null if the user profile does not have one for the given experiment - */ -DecisionService.prototype.__getStoredVariation = function(experiment, userId, experimentBucketMap) { - if (experimentBucketMap.hasOwnProperty(experiment.id)) { - var decision = experimentBucketMap[experiment.id]; - var variationId = decision.variation_id; - if (this.configObj.variationIdMap.hasOwnProperty(variationId)) { - return this.configObj.variationIdMap[decision.variation_id]; - } else { - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.SAVED_VARIATION_NOT_FOUND, MODULE_NAME, userId, variationId, experiment.key)); - } - } - - return null; -}; - -/** - * Get the user profile with the given user ID - * @param {string} userId - * @return {Object|undefined} the stored user profile or undefined if one isn't found - */ -DecisionService.prototype.__getUserProfile = function(userId) { - var userProfile = { - user_id: userId, - experiment_bucket_map: {}, - }; - - if (!this.userProfileService) { - return userProfile; - } - - try { - return this.userProfileService.lookup(userId); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.USER_PROFILE_LOOKUP_ERROR, MODULE_NAME, userId, ex.message)); - } -}; - -/** - * Saves the bucketing decision to the user profile - * @param {Object} userProfile - * @param {Object} experiment - * @param {Object} variation - * @param {Object} experimentBucketMap - */ -DecisionService.prototype.__saveUserProfile = function(experiment, variation, userId, experimentBucketMap) { - if (!this.userProfileService) { - return; - } - - try { - var newBucketMap = fns.cloneDeep(experimentBucketMap); - newBucketMap[experiment.id] = { - variation_id: variation.id - }; - - this.userProfileService.save({ - user_id: userId, - experiment_bucket_map: newBucketMap, - }); - - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.SAVED_VARIATION, MODULE_NAME, variation.key, experiment.key, userId)); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.USER_PROFILE_SAVE_ERROR, MODULE_NAME, userId, ex.message)); - } -}; - -/** - * Given a feature, user ID, and attributes, returns an object representing a - * decision. If the user was bucketed into a variation for the given feature - * and attributes, the returned decision object will have variation and - * experiment properties (both objects), as well as a decisionSource property. - * decisionSource indicates whether the decision was due to a rollout or an - * experiment. - * @param {Object} feature A feature flag object from project configuration - * @param {String} userId A string identifying the user, for bucketing - * @param {Object} attributes Optional user attributes - * @return {Object} An object with experiment, variation, and decisionSource - * properties. If the user was not bucketed into a variation, the variation - * property is null. - */ -DecisionService.prototype.getVariationForFeature = function(feature, userId, attributes) { - var experimentDecision = this._getVariationForFeatureExperiment(feature, userId, attributes); - if (experimentDecision.variation !== null) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_IN_FEATURE_EXPERIMENT, MODULE_NAME, userId, experimentDecision.variation.key, experimentDecision.experiment.key, feature.key)); - return experimentDecision; - } - - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_NOT_IN_FEATURE_EXPERIMENT, MODULE_NAME, userId, feature.key)); - - var rolloutDecision = this._getVariationForRollout(feature, userId, attributes); - if (rolloutDecision.variation !== null) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_IN_ROLLOUT, MODULE_NAME, userId, feature.key)); - return rolloutDecision; - } - - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_NOT_IN_ROLLOUT, MODULE_NAME, userId, feature.key)); - - return { - experiment: null, - variation: null, - decisionSource: null, - }; -}; - -DecisionService.prototype._getVariationForFeatureExperiment = function(feature, userId, attributes) { - var experiment = null; - var variationKey = null; - - if (feature.hasOwnProperty('groupId')) { - var group = this.configObj.groupIdMap[feature.groupId]; - if (group) { - experiment = this._getExperimentInGroup(group, userId); - if (experiment && feature.experimentIds.indexOf(experiment.id) !== -1) { - variationKey = this.getVariation(experiment.key, userId, attributes); - } - } - } else if (feature.experimentIds.length > 0) { - // If the feature does not have a group ID, then it can only be associated - // with one experiment, so we look at the first experiment ID only - experiment = projectConfig.getExperimentFromId(this.configObj, feature.experimentIds[0], this.logger); - if (experiment) { - variationKey = this.getVariation(experiment.key, userId, attributes); - } - } else { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.FEATURE_HAS_NO_EXPERIMENTS, MODULE_NAME, feature.key)); - } - - var variation = null; - if (variationKey !== null && experiment !== null) { - variation = experiment.variationKeyMap[variationKey]; - } - return { - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }; -}; - -DecisionService.prototype._getExperimentInGroup = function(group, userId) { - var experimentId = bucketer.bucketUserIntoExperiment(group, userId, userId, this.logger); - if (experimentId !== null) { - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP, MODULE_NAME, userId, experimentId, group.id)); - var experiment = projectConfig.getExperimentFromId(this.configObj, experimentId, this.logger); - if (experiment) { - return experiment; - } - } - - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.USER_NOT_BUCKETED_INTO_ANY_EXPERIMENT_IN_GROUP, MODULE_NAME, userId, group.id)); - return null; -}; - -DecisionService.prototype._getVariationForRollout = function(feature, userId, attributes) { - if (!feature.rolloutId) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.NO_ROLLOUT_EXISTS, MODULE_NAME, feature.key)); - return { - experiment: null, - variation: null, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - } - - var rollout = this.configObj.rolloutIdMap[feature.rolloutId]; - if (!rollout) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.INVALID_ROLLOUT_ID, MODULE_NAME, feature.rolloutId, feature.key)); - return { - experiment: null, - variation: null, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - } - - if (rollout.experiments.length === 0) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.ROLLOUT_HAS_NO_EXPERIMENTS, MODULE_NAME, feature.rolloutId)); - return { - experiment: null, - variation: null, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - } - - var bucketingId = this._getBucketingId(userId, attributes); - - // The end index is length - 1 because the last experiment is assumed to be - // "everyone else", which will be evaluated separately outside this loop - var endIndex = rollout.experiments.length - 1; - var index; - var experiment; - var bucketerParams; - var variationId; - var variation; - for (index = 0; index < endIndex; index++) { - experiment = this.configObj.experimentKeyMap[rollout.experiments[index].key]; - - if (!this.__checkIfUserIsInAudience(experiment.key, userId, attributes)) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE, MODULE_NAME, userId, index + 1)); - continue; - } - - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_MEETS_CONDITIONS_FOR_TARGETING_RULE, MODULE_NAME, userId, index + 1)); - bucketerParams = this.__buildBucketerParams(experiment.key, bucketingId, userId); - variationId = bucketer.bucket(bucketerParams); - variation = this.configObj.variationIdMap[variationId]; - if (variation) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_BUCKETED_INTO_TARGETING_RULE, MODULE_NAME, userId, index + 1)); - return { - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - } else { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_NOT_BUCKETED_INTO_TARGETING_RULE, MODULE_NAME, userId, index + 1)); - break; - } - } - - var everyoneElseExperiment = this.configObj.experimentKeyMap[rollout.experiments[endIndex].key]; - if (this.__checkIfUserIsInAudience(everyoneElseExperiment.key, userId, attributes)) { - bucketerParams = this.__buildBucketerParams(everyoneElseExperiment.key, bucketingId, userId); - variationId = bucketer.bucket(bucketerParams); - variation = this.configObj.variationIdMap[variationId]; - if (variation) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_BUCKETED_INTO_EVERYONE_TARGETING_RULE, MODULE_NAME, userId)); - return { - experiment: everyoneElseExperiment, - variation: variation, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - } else { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_NOT_BUCKETED_INTO_EVERYONE_TARGETING_RULE, MODULE_NAME, userId)); - } - } - - return { - experiment: null, - variation: null, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; -}; - -/** - * Get bucketing Id from user attributes. - * @param {String} userId - * @param {Object} attributes - * @returns {String} Bucketing Id if it is a string type in attributes, user Id otherwise. - */ -DecisionService.prototype._getBucketingId = function(userId, attributes) { - var bucketingId = userId; - - // If the bucketing ID key is defined in attributes, than use that in place of the userID for the murmur hash key - if ((attributes != null && typeof attributes === 'object') && attributes.hasOwnProperty(enums.CONTROL_ATTRIBUTES.BUCKETING_ID)) { - if (typeof attributes[enums.CONTROL_ATTRIBUTES.BUCKETING_ID] === 'string') { - bucketingId = attributes[enums.CONTROL_ATTRIBUTES.BUCKETING_ID]; - this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.VALID_BUCKETING_ID, MODULE_NAME, bucketingId)); - } else { - this.logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.BUCKETING_ID_NOT_STRING, MODULE_NAME)); - } - } - - return bucketingId; -}; - -module.exports = { - /** - * Creates an instance of the DecisionService. - * @param {Object} options Configuration options - * @param {Object} options.configObj - * @param {Object} options.userProfileService - * @param {Object} options.logger - * @return {Object} An instance of the DecisionService - */ - createDecisionService: function(options) { - return new DecisionService(options); - }, -}; diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js deleted file mode 100644 index 1b8151be2..000000000 --- a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js +++ /dev/null @@ -1,1438 +0,0 @@ -/**************************************************************************** - * Copyright 2017-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var Optimizely = require('../../optimizely'); -var eventBuilder = require('../../core/event_builder/index.js'); -var eventDispatcher = require('../../plugins/event_dispatcher/index.node'); -var errorHandler = require('../../plugins/error_handler'); -var bucketer = require('../bucketer'); -var DecisionService = require('./'); -var enums = require('../../utils/enums'); -var logger = require('../../plugins/logger'); -var projectConfig = require('../project_config'); -var sprintf = require('sprintf-js').sprintf; -var testData = require('../../tests/test_data').getTestProjectConfig(); -var testDataWithFeatures = require('../../tests/test_data').getTestProjectConfigWithFeatures(); -var jsonSchemaValidator = require('../../utils/json_schema_validator'); -var audienceEvaluator = require('../audience_evaluator'); - -var chai = require('chai'); -var sinon = require('sinon'); -var assert = chai.assert; - -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var DECISION_SOURCES = enums.DECISION_SOURCES; - -describe('lib/core/decision_service', function() { - describe('APIs', function() { - var configObj = projectConfig.createProjectConfig(testData); - var decisionServiceInstance; - var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - var bucketerStub; - - beforeEach(function () { - bucketerStub = sinon.stub(bucketer, 'bucket'); - sinon.stub(mockLogger, 'log'); - decisionServiceInstance = DecisionService.createDecisionService({ - configObj: configObj, - logger: mockLogger, - }); - }); - - afterEach(function () { - bucketer.bucket.restore(); - mockLogger.log.restore(); - }); - - describe('#getVariation', function () { - it('should return the correct variation for the given experiment key and user ID for a running experiment', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation from `test_data` - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledOnce(bucketerStub); - }); - - it('should return the whitelisted variation if the user is whitelisted', function () { - assert.strictEqual('variationWithAudience', decisionServiceInstance.getVariation('testExperimentWithAudiences', 'user2')); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User user2 is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: User user2 is forced in variation variationWithAudience.'); - }); - - it('should return null if the user does not meet audience conditions', function () { - assert.isNull(decisionServiceInstance.getVariation('testExperimentWithAudiences', 'user3', {foo: 'bar'})); - assert.strictEqual(7, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User user3 is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[5][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); - assert.strictEqual(mockLogger.log.args[6][1], 'DECISION_SERVICE: User user3 does not meet conditions to be in experiment testExperimentWithAudiences.'); - }); - - it('should return null if the experiment is not running', function () { - assert.isNull(decisionServiceInstance.getVariation('testExperimentNotRunning', 'user1')); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(1, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Experiment testExperimentNotRunning is not running.'); - }); - - describe('when attributes.$opt_experiment_bucket_map is supplied', function() { - it('should respect the sticky bucketing information for attributes', function() { - bucketerStub.returns('111128'); // ID of the 'control' variation from `test_data` - var attributes = { - $opt_experiment_bucket_map: { - '111127': { - 'variation_id': '111129' // ID of the 'variation' variation - }, - }, - }; - - assert.strictEqual('variation', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user', attributes)); - sinon.assert.notCalled(bucketerStub); - }); - }); - - describe('when a user profile service is provided', function () { - var userProfileServiceInstance = null; - var userProfileLookupStub; - var userProfileSaveStub; - beforeEach(function () { - userProfileServiceInstance = { - lookup: function () { - }, - save: function () { - }, - }; - - decisionServiceInstance = DecisionService.createDecisionService({ - configObj: configObj, - logger: mockLogger, - userProfileService: userProfileServiceInstance, - }); - userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); - userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save'); - sinon.stub(decisionServiceInstance, '__getWhitelistedVariation').returns(null); - }); - - afterEach(function () { - userProfileServiceInstance.lookup.restore(); - userProfileServiceInstance.save.restore(); - decisionServiceInstance.__getWhitelistedVariation.restore(); - }); - - it('should return the previously bucketed variation', function () { - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128' // ID of the 'control' variation - }, - }, - }); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Returning previously activated variation \"control\" of experiment \"testExperiment\" for user \"decision_service_user\" from user profile.'); - }); - - it('should bucket if there was no prevously bucketed variation', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: {}, - }); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.calledOnce(bucketerStub); - // make sure we save the decision - sinon.assert.calledWith(userProfileSaveStub, { - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128', - } - }, - }); - }); - - it('should bucket if the user profile service returns null', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation - userProfileLookupStub.returns(null); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.calledOnce(bucketerStub); - // make sure we save the decision - sinon.assert.calledWith(userProfileSaveStub, { - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128', - } - }, - }); - }); - - it('should re-bucket if the stored variation is no longer valid', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': 'not valid variation', - }, - }, - }); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.calledOnce(bucketerStub); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: User decision_service_user was previously bucketed into variation with ID not valid variation for experiment testExperiment, but no matching variation was found.'); - // make sure we save the decision - sinon.assert.calledWith(userProfileSaveStub, { - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128', - } - }, - }); - }); - - it('should store the bucketed variation for the user', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: {}, // no decisions for user - }); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.calledOnce(bucketerStub); - assert.strictEqual(4, mockLogger.log.callCount); - sinon.assert.calledWith(userProfileServiceInstance.save, { - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - variation_id: '111128', - }, - }, - }); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: Saved variation "control" of experiment "testExperiment" for user "decision_service_user".'); - }); - - it('should log an error message if "lookup" throws an error', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation - userProfileLookupStub.throws(new Error('I am an error')); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Error while looking up user profile for user ID "decision_service_user": I am an error.'); - }); - - it('should log an error message if "save" throws an error', function () { - bucketerStub.returns('111128'); // ID of the 'control' variation - userProfileLookupStub.returns(null); - userProfileSaveStub.throws(new Error('I am an error')); - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user')); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing - - assert.strictEqual(4, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: Error while saving user profile for user ID "decision_service_user": I am an error.'); - - // make sure that we save the decision - sinon.assert.calledWith(userProfileSaveStub, { - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128', - } - }, - }); - }); - - describe('when passing `attributes.$opt_experiment_bucket_map`', function() { - it('should respect attributes over the userProfileService for the matching experiment id', function () { - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128' // ID of the 'control' variation - }, - }, - }); - - var attributes = { - $opt_experiment_bucket_map: { - '111127': { - 'variation_id': '111129' // ID of the 'variation' variation - }, - }, - }; - - - assert.strictEqual('variation', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user', attributes)); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Returning previously activated variation \"variation\" of experiment \"testExperiment\" for user \"decision_service_user\" from user profile.'); - }); - - it('should ignore attributes for a different experiment id', function () { - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: { - '111127': { // 'testExperiment' ID - 'variation_id': '111128' // ID of the 'control' variation - }, - }, - }); - - var attributes = { - $opt_experiment_bucket_map: { - '122227': { // other experiment ID - 'variation_id': '122229' // ID of the 'variationWithAudience' variation - }, - }, - }; - - assert.strictEqual('control', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user', attributes)); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Returning previously activated variation \"control\" of experiment \"testExperiment\" for user \"decision_service_user\" from user profile.'); - }); - - it('should use attributes when the userProfileLookup variations for other experiments', function () { - userProfileLookupStub.returns({ - user_id: 'decision_service_user', - experiment_bucket_map: { - '122227': { // other experiment ID - 'variation_id': '122229' // ID of the 'variationWithAudience' variation - }, - } - }); - - var attributes = { - $opt_experiment_bucket_map: { - '111127': { // 'testExperiment' ID - 'variation_id': '111129' // ID of the 'variation' variation - }, - }, - }; - - assert.strictEqual('variation', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user', attributes)); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Returning previously activated variation \"variation\" of experiment \"testExperiment\" for user \"decision_service_user\" from user profile.'); - }); - - it('should use attributes when the userProfileLookup returns null', function () { - userProfileLookupStub.returns(null); - - var attributes = { - $opt_experiment_bucket_map: { - '111127': { - 'variation_id': '111129' // ID of the 'variation' variation - }, - }, - }; - - assert.strictEqual('variation', decisionServiceInstance.getVariation('testExperiment', 'decision_service_user', attributes)); - sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user'); - sinon.assert.notCalled(bucketerStub); - assert.strictEqual(mockLogger.log.args[0][1], 'PROJECT_CONFIG: User decision_service_user is not in the forced variation map.'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Returning previously activated variation \"variation\" of experiment \"testExperiment\" for user \"decision_service_user\" from user profile.'); - }); - }); - }); - }); - - describe('__buildBucketerParams', function () { - it('should return params object with correct properties', function () { - var bucketerParams = decisionServiceInstance.__buildBucketerParams('testExperiment', 'testUser', 'testUser'); - - var expectedParams = { - bucketingId: 'testUser', - experimentKey: 'testExperiment', - userId: 'testUser', - experimentId: '111127', - trafficAllocationConfig: [ - { - entityId: '111128', - endOfRange: 4000, - }, - { - entityId: '111129', - endOfRange: 9000, - }, - ], - variationIdMap: configObj.variationIdMap, - logger: mockLogger, - experimentKeyMap: configObj.experimentKeyMap, - groupIdMap: configObj.groupIdMap, - }; - - assert.deepEqual(bucketerParams, expectedParams); - - sinon.assert.notCalled(mockLogger.log); - }); - }); - - describe('__checkIfExperimentIsActive', function () { - it('should return true if experiment is running', function () { - assert.isTrue(decisionServiceInstance.__checkIfExperimentIsActive('testExperiment', 'testUser')); - sinon.assert.notCalled(mockLogger.log); - }); - - it('should return false when experiment is not running', function () { - assert.isFalse(decisionServiceInstance.__checkIfExperimentIsActive('testExperimentNotRunning', 'testUser')); - sinon.assert.calledOnce(mockLogger.log); - var logMessage = mockLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning')); - }); - }); - - describe('__checkIfUserIsInAudience', function () { - var __audienceEvaluateSpy; - - beforeEach(function() { - __audienceEvaluateSpy = sinon.spy(audienceEvaluator, 'evaluate'); - }); - - afterEach(function() { - __audienceEvaluateSpy.restore(); - }); - - it('should return true when audience conditions are met', function () { - assert.isTrue(decisionServiceInstance.__checkIfUserIsInAudience('testExperimentWithAudiences', 'testUser', {browser_type: 'firefox'})); - assert.strictEqual(4, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to TRUE.'); - }); - - it('should return true when experiment has no audience', function () { - assert.isTrue(decisionServiceInstance.__checkIfUserIsInAudience('testExperiment', 'testUser')); - assert.isTrue(__audienceEvaluateSpy.alwaysReturned(true)); - - assert.strictEqual(2, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperiment": [].'); - assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Audiences for experiment testExperiment collectively evaluated to TRUE.'); - }); - - it('should return false when audience conditions can not be evaluated', function() { - assert.isFalse(decisionServiceInstance.__checkIfUserIsInAudience('testExperimentWithAudiences', 'testUser')); - assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false)); - - assert.strictEqual(6, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[4][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); - assert.strictEqual(mockLogger.log.args[5][1], 'DECISION_SERVICE: User testUser does not meet conditions to be in experiment testExperimentWithAudiences.'); - }); - - it('should return false when audience conditions are not met', function () { - assert.isFalse(decisionServiceInstance.__checkIfUserIsInAudience('testExperimentWithAudiences', 'testUser', {browser_type: 'chrome'})); - assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false)); - - assert.strictEqual(5, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); - assert.strictEqual(mockLogger.log.args[4][1], 'DECISION_SERVICE: User testUser does not meet conditions to be in experiment testExperimentWithAudiences.'); - }); - }); - - describe('__getWhitelistedVariation', function () { - it('should return forced variation ID if forced variation is provided for the user ID', function () { - var testExperiment = configObj.experimentKeyMap['testExperiment']; - var expectedVariation = configObj.variationIdMap['111128']; - assert.strictEqual(decisionServiceInstance.__getWhitelistedVariation(testExperiment, 'user1'), expectedVariation); - }); - - it('should return null if forced variation is not provided for the user ID', function () { - var testExperiment = configObj.experimentKeyMap['testExperiment']; - assert.isNull(decisionServiceInstance.__getWhitelistedVariation(testExperiment, 'notInForcedVariations')); - }); - }); - }); - - describe('when a bucketingID is provided', function() { - var configObj = projectConfig.createProjectConfig(testData); - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.DEBUG, - logToConsole: false, - }); - var optlyInstance; - beforeEach(function () { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData, - jsonSchemaValidator: jsonSchemaValidator, - isValidInstance: true, - logger: createdLogger, - eventBuilder: eventBuilder, - eventDispatcher: eventDispatcher, - errorHandler: errorHandler, - }); - - sinon.stub(eventDispatcher, 'dispatchEvent'); - sinon.stub(errorHandler, 'handleError'); - }); - - afterEach(function () { - eventDispatcher.dispatchEvent.restore(); - errorHandler.handleError.restore(); - }); - - var testUserAttributes = { - 'browser_type': 'firefox', - }; - var userAttributesWithBucketingId = { - 'browser_type': 'firefox', - '$opt_bucketing_id': '123456789' - }; - var invalidUserAttributesWithBucketingId = { - 'browser_type': 'safari', - '$opt_bucketing_id': 'testBucketingIdControl!' - }; - - it('confirm normal bucketing occurs before setting bucketingId', function () { - assert.strictEqual('variation', optlyInstance.getVariation( - 'testExperiment', - 'test_user', - testUserAttributes)); - }); - - it('confirm valid bucketing with bucketing ID set in attributes', function () { - assert.strictEqual('variationWithAudience', optlyInstance.getVariation( - 'testExperimentWithAudiences', - 'test_user', - userAttributesWithBucketingId - )); - }); - - it('check invalid audience with bucketingId', function () { - assert.strictEqual(null, optlyInstance.getVariation( - 'testExperimentWithAudiences', - 'test_user', - invalidUserAttributesWithBucketingId - )); - }); - - it('test that an experiment that is not running returns a null variation', function () { - assert.strictEqual(null, optlyInstance.getVariation( - 'testExperimentNotRunning', - 'test_user', - userAttributesWithBucketingId - )); - }); - - it('test that an invalid experiment key gets a null variation', function () { - assert.strictEqual(null, optlyInstance.getVariation( - 'invalidExperiment', - 'test_user', - userAttributesWithBucketingId - )); - }); - - it('check forced variation', function () { - assert.isTrue(optlyInstance.setForcedVariation( - 'testExperiment', - 'test_user', - 'control'), - sprintf('Set variation to "%s" failed', 'control') - ); - assert.strictEqual('control', optlyInstance.getVariation( - 'testExperiment', - 'test_user', - userAttributesWithBucketingId - )); - }); - - it('check whitelisted variation', function () { - assert.strictEqual('control', optlyInstance.getVariation( - 'testExperiment', - 'user1', - userAttributesWithBucketingId - )); - }); - - it('check user profile', function () { - var userProfileLookupStub; - var userProfileServiceInstance = { - lookup: function () { - }, - }; - userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup'); - userProfileLookupStub.returns({ - user_id: 'test_user', - experiment_bucket_map: { - '111127': { - 'variation_id': '111128' // ID of the 'control' variation - }, - }, - }); - - var decisionServiceInstance = DecisionService.createDecisionService({ - configObj: configObj, - logger: createdLogger, - userProfileService: userProfileServiceInstance, - }); - - assert.strictEqual('control', decisionServiceInstance.getVariation( - 'testExperiment', - 'test_user', - userAttributesWithBucketingId - )); - sinon.assert.calledWithExactly(userProfileLookupStub, 'test_user'); - }); - }); - - describe('_getBucketingId', function() { - var configObj; - var decisionService; - var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - var userId = 'testUser1'; - var userAttributesWithBucketingId = { - 'browser_type': 'firefox', - '$opt_bucketing_id': '123456789' - }; - var userAttributesWithInvalidBucketingId = { - 'browser_type': 'safari', - '$opt_bucketing_id': 50 - }; - - beforeEach(function() { - sinon.stub(mockLogger, 'log'); - configObj = projectConfig.createProjectConfig(testData); - decisionService = DecisionService.createDecisionService({ - configObj: configObj, - logger: mockLogger, - }); - }); - - afterEach(function() { - mockLogger.log.restore(); - }); - - it('should return userId if bucketingId is not defined in user attributes', function() { - assert.strictEqual(userId, decisionService._getBucketingId(userId, null)); - assert.strictEqual(userId, decisionService._getBucketingId(userId, {'browser_type': 'safari'})); - }); - - it('should log warning in case of invalid bucketingId', function() { - assert.strictEqual(userId, decisionService._getBucketingId(userId, userAttributesWithInvalidBucketingId)); - assert.strictEqual(1, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: BucketingID attribute is not a string. Defaulted to userId'); - }); - - it('should return correct bucketingId when provided in attributes', function() { - assert.strictEqual('123456789', decisionService._getBucketingId(userId, userAttributesWithBucketingId)); - assert.strictEqual(1, mockLogger.log.callCount); - assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: BucketingId is valid: "123456789"'); - }); - }); - - describe('feature management', function() { - describe('#getVariationForFeature', function() { - var configObj; - var decisionServiceInstance; - var sandbox; - var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDataWithFeatures); - sandbox = sinon.sandbox.create(); - sandbox.stub(mockLogger, 'log'); - decisionServiceInstance = DecisionService.createDecisionService({ - configObj: configObj, - logger: mockLogger, - }); - }); - - afterEach(function() { - sandbox.restore(); - }); - - describe('feature attached to an experiment, and not attached to a rollout', function() { - var feature; - beforeEach(function() { - feature = configObj.featureKeyMap.test_feature_for_experiment; - }); - - describe('user bucketed into this experiment', function() { - var getVariationStub; - beforeEach(function() { - getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation'); - getVariationStub.returns(null); - getVariationStub.withArgs('testing_my_feature', 'user1').returns('variation'); - }); - - it('returns a decision with a variation in the experiment the feature is attached to', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1', { - test_attribute: 'test_value', - }); - var expectedDecision = { - experiment: { - 'forcedVariations': {}, - 'status': 'Running', - 'key': 'testing_my_feature', - 'id': '594098', - 'variations': [ - { - 'id': '594096', - 'variables': [ - { - 'id': '4792309476491264', - 'value': '2' - }, - { - 'id': '5073784453201920', - 'value': 'true' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me NOW' - }, - { - 'id': '6199684360044544', - 'value': '20.25' - } - ], - 'featureEnabled': true, - 'key': 'variation' - }, - { - 'id': '594097', - 'variables': [ - { - 'id': '4792309476491264', - 'value': '10' - }, - { - 'id': '5073784453201920', - 'value': 'false' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me' - }, - { - 'id': '6199684360044544', - 'value': '50.55' - } - ], - 'featureEnabled': true, - 'key': 'control' - } - ], - 'audienceIds': [], - 'trafficAllocation': [ - { 'endOfRange': 5000, 'entityId': '594096' }, - { 'endOfRange': 10000, 'entityId': '594097' } - ], - 'layerId': '594093', - variationKeyMap: { - control: { - 'id': '594097', - 'variables': [ - { - 'id': '4792309476491264', - 'value': '10' - }, - { - 'id': '5073784453201920', - 'value': 'false' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me' - }, - { - 'id': '6199684360044544', - 'value': '50.55' - } - ], - 'featureEnabled': true, - 'key': 'control' - }, - variation: { - 'id': '594096', - 'variables': [ - { - 'id': '4792309476491264', - 'value': '2' - }, - { - 'id': '5073784453201920', - 'value': 'true' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me NOW' - }, - { - 'id': '6199684360044544', - 'value': '20.25' - } - ], - 'featureEnabled': true, - 'key': 'variation' - }, - }, - }, - variation: { - 'id': '594096', - 'variables': [ - { - 'id': '4792309476491264', - 'value': '2' - }, - { - 'id': '5073784453201920', - 'value': 'true' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me NOW' - }, - { - 'id': '6199684360044544', - 'value': '20.25' - } - ], - 'featureEnabled': true, - 'key': 'variation' - }, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is in variation variation of experiment testing_my_feature on the feature test_feature_for_experiment.'); - sinon.assert.calledWithExactly(getVariationStub, 'testing_my_feature', 'user1', { - test_attribute: 'test_value', - }); - }); - }); - - describe('user not bucketed into this experiment', function() { - var getVariationStub; - beforeEach(function() { - getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation'); - getVariationStub.returns(null); - }); - - it('returns a decision with no variation', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: null, - variation: null, - decisionSource: null, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in any experiment on the feature test_feature_for_experiment.'); - }); - }); - }); - - describe('feature attached to an experiment in a group, and not attached to a rollout', function() { - var feature; - beforeEach(function() { - feature = configObj.featureKeyMap.feature_with_group; - }); - - describe('user bucketed into an experiment in the group', function() { - var getVariationStub; - beforeEach(function() { - getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation'); - getVariationStub.returns(null); - getVariationStub.withArgs('exp_with_group', 'user1').returns('var'); - }); - - it('returns a decision with a variation in an experiment in a group', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: { - 'forcedVariations': {}, - 'status': 'Running', - 'key': 'exp_with_group', - 'id': '595010', - 'variations': [{ 'id': '595008', 'variables': [], 'key': 'var' }, { 'id': '595009', 'variables': [], 'key': 'con' }], - 'audienceIds': [], - 'trafficAllocation': [{ 'endOfRange': 5000, 'entityId': '595008' }, { 'endOfRange': 10000, 'entityId': '595009' }], - 'layerId': '595005', - groupId: '595024', - variationKeyMap: { - con: { - 'id': '595009', - 'variables': [], - 'key': 'con', - }, - var: { - 'id': '595008', - 'variables': [], - 'key': 'var', - }, - }, - }, - variation: { - 'id': '595008', - 'variables': [], - 'key': 'var', - }, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is in variation var of experiment exp_with_group on the feature feature_with_group.'); - }); - }); - - describe('user not bucketed into an experiment in the group', function() { - var getVariationStub; - beforeEach(function() { - getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation'); - getVariationStub.returns(null); - }); - - it('returns a decision with no experiment and no variation', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: null, - variation: null, - decisionSource: null, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in any experiment on the feature feature_with_group.'); - }); - - it('returns null decision for group experiment not referenced by the feature', function() { - var noTrafficExpFeature = configObj.featureKeyMap.feature_exp_no_traffic; - var decision = decisionServiceInstance.getVariationForFeature(noTrafficExpFeature, 'user1'); - var expectedDecision = { - experiment: null, - variation: null, - decisionSource: null, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in any experiment on the feature feature_exp_no_traffic.'); - }); - }); - - describe('user not bucketed into the group', function() { - var bucketUserIntoExperimentStub; - beforeEach(function() { - bucketUserIntoExperimentStub = sandbox.stub(bucketer, 'bucketUserIntoExperiment'); - bucketUserIntoExperimentStub.returns(null); - }); - - it('returns a decision with no experiment and no variation', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: null, - variation: null, - decisionSource: null, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in any experiment on the feature feature_with_group.'); - }); - }); - }); - - describe('feature attached to a rollout', function() { - var feature; - var bucketStub; - beforeEach(function() { - feature = configObj.featureKeyMap.test_feature; - bucketStub = sandbox.stub(bucketer, 'bucket'); - }); - - describe('user bucketed into an audience targeting rule', function() { - beforeEach(function() { - bucketStub.returns('594032'); // ID of variation in rollout experiment - audience targeting rule for 'test_audience' - }); - - it('returns a decision with a variation and experiment from the audience targeting rule', function() { - var attributes = { test_attribute: 'test_value' }; - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1', attributes); - var expectedDecision = { - experiment: { - 'forcedVariations': {}, - 'status': 'Not started', - 'key': '594031', - 'id': '594031', - 'variations': [{ - 'id': '594032', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'true' - }, - { - 'id': '5482802778734592', - 'value': '395' - }, - { - 'id': '6045752732155904', - 'value': '4.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello audience' - } - ], - 'featureEnabled': true, - 'key': '594032' - }], - variationKeyMap: { - 594032: { - 'id': '594032', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'true' - }, - { - 'id': '5482802778734592', - 'value': '395' - }, - { - 'id': '6045752732155904', - 'value': '4.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello audience' - } - ], - 'featureEnabled': true, - 'key': '594032' - }, - }, - 'audienceIds': ['594017'], - 'trafficAllocation': [{ 'endOfRange': 5000, 'entityId': '594032' }], - 'layerId': '594030' - }, - variation: { - 'id': '594032', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'true' - }, - { - 'id': '5482802778734592', - 'value': '395' - }, - { - 'id': '6045752732155904', - 'value': '4.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello audience' - } - ], - 'featureEnabled': true, - 'key': '594032' - }, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 meets conditions for targeting rule 1.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 bucketed into targeting rule 1.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is in rollout of feature test_feature.'); - }); - }); - - describe('user bucketed into everyone else targeting rule', function() { - beforeEach(function() { - bucketStub.returns('594038'); // ID of variation in rollout experiment - everyone else targeting rule - }); - - it('returns a decision with a variation and experiment from the everyone else targeting rule', function() { - var attributes = {}; - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1', attributes); - var expectedDecision = { - experiment: { - 'forcedVariations': {}, - 'status': 'Not started', - 'key': '594037', - 'id': '594037', - 'variations': [{ - 'id': '594038', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ], - 'featureEnabled': false, - 'key': '594038' - }], - 'audienceIds': [], - 'trafficAllocation': [{ 'endOfRange': 0, 'entityId': '594038' }], - 'layerId': '594030', - variationKeyMap: { - 594038: { - 'id': '594038', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ], - 'featureEnabled': false, - 'key': '594038' - }, - }, - }, - variation: { - 'id': '594038', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ], - 'featureEnabled': false, - 'key': '594038' - }, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 does not meet conditions for targeting rule 1.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 bucketed into everyone targeting rule.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is in rollout of feature test_feature.'); - }); - }); - - describe('user not bucketed into audience targeting rule or everyone else rule', function() { - beforeEach(function() { - bucketStub.returns(null); - }); - - it('returns a decision with no variation and no experiment', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: null, - variation: null, - decisionSource: null, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 does not meet conditions for targeting rule 1.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in rollout of feature test_feature.'); - }); - }); - - describe('user excluded from audience targeting rule due to traffic allocation, and bucketed into everyone else', function() { - beforeEach(function() { - bucketStub.returns(null); // returns no variation for other calls - bucketStub.withArgs(sinon.match({ - experimentKey: '594037', - })).returns('594038'); // returns variation from everyone else targeitng rule when called with everyone else experiment key; - }); - - it('returns a decision with a variation and experiment from the everyone else targeting rule', function() { - var attributes = { test_attribute: 'test_value' }; - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1', attributes); - var expectedDecision = { - experiment: { - 'forcedVariations': {}, - 'status': 'Not started', - 'key': '594037', - 'id': '594037', - 'variations': [{ - 'id': '594038', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ], - 'featureEnabled': false, - 'key': '594038' - }], - 'audienceIds': [], - 'trafficAllocation': [{ 'endOfRange': 0, 'entityId': '594038' }], - 'layerId': '594030', - variationKeyMap: { - 594038: { - 'id': '594038', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ], - 'featureEnabled': false, - 'key': '594038' - }, - }, - }, - variation: { - 'id': '594038', - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ], - 'featureEnabled': false, - 'key': '594038' - }, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 meets conditions for targeting rule 1.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE User user1 not bucketed into targeting rule 1 due to traffic allocation. Trying everyone rule.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 bucketed into everyone targeting rule.'); - }); - }); - }); - - describe('feature attached to both an experiment and a rollout', function() { - var feature; - var getVariationStub; - var bucketStub; - beforeEach(function() { - feature = configObj.featureKeyMap.shared_feature; - getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation'); - getVariationStub.returns(null); // No variation returned by getVariation - bucketStub = sandbox.stub(bucketer, 'bucket'); - bucketStub.returns('599057'); // Id of variation in rollout of shared feature - }); - - it('can bucket a user into the rollout when the user is not bucketed into the experiment', function() { - // No attributes passed, so user is not in the audience for the experiment - // It should fall through to the rollout - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: { - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '599057' - } - ], - 'layerId': '599055', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': '599057', - 'id': '599057', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '200' - }, - { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - } - ] - } - ], - 'status': 'Not started', - 'key': '599056', - 'id': '599056', - variationKeyMap: { - 599057: { - 'key': '599057', - 'id': '599057', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '200' - }, - { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - } - ] - } - } - }, - variation: { - 'key': '599057', - 'id': '599057', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '200' - }, - { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - } - ] - }, - decisionSource: DECISION_SOURCES.ROLLOUT, - }; - assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in any experiment on the feature shared_feature.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 bucketed into everyone targeting rule.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is in rollout of feature shared_feature.'); - }); - }); - - describe('feature not attached to an experiment or a rollout', function() { - var feature; - beforeEach(function() { - feature = configObj.featureKeyMap.unused_flag; - }); - - it('returns a decision with no variation and no experiment', function() { - var decision = decisionServiceInstance.getVariationForFeature(feature, 'user1'); - var expectedDecision = { - experiment: null, - variation: null, - decisionSource: null, - }; - var expectedDecision = assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: Feature unused_flag is not attached to any experiments.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: User user1 is not in any experiment on the feature unused_flag.'); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'DECISION_SERVICE: There is no rollout of feature unused_flag.'); - }); - }); - }); - - describe('_getVariationForRollout', function() { - var feature; - var configObj; - var decisionService; - var __buildBucketerParamsSpy; - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDataWithFeatures); - feature = configObj.featureKeyMap.test_feature; - decisionService = DecisionService.createDecisionService({ - configObj: configObj, - logger: logger.createLogger({logLevel: LOG_LEVEL.INFO}), - }); - __buildBucketerParamsSpy = sinon.spy(decisionService, '__buildBucketerParams'); - }); - - afterEach(function() { - __buildBucketerParamsSpy.restore(); - }); - - it('should call __buildBucketerParams with user Id when bucketing Id is not provided in the attributes', function () { - var attributes = { test_attribute: 'test_value' }; - decisionService._getVariationForRollout(feature, 'testUser', attributes); - - sinon.assert.callCount(__buildBucketerParamsSpy, 2); - sinon.assert.calledWithExactly(__buildBucketerParamsSpy, '594031', 'testUser', 'testUser'); - sinon.assert.calledWithExactly(__buildBucketerParamsSpy, '594037', 'testUser', 'testUser'); - }); - - it('should call __buildBucketerParams with bucketing Id when bucketing Id is provided in the attributes', function () { - var attributes = { - test_attribute: 'test_value', - $opt_bucketing_id: 'abcdefg' - }; - decisionService._getVariationForRollout(feature, 'testUser', attributes); - - sinon.assert.callCount(__buildBucketerParamsSpy, 2); - sinon.assert.calledWithExactly(__buildBucketerParamsSpy, '594031', 'abcdefg', 'testUser'); - sinon.assert.calledWithExactly(__buildBucketerParamsSpy, '594037', 'abcdefg', 'testUser'); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.js b/packages/optimizely-sdk/lib/core/event_builder/index.js deleted file mode 100644 index cc977d3e7..000000000 --- a/packages/optimizely-sdk/lib/core/event_builder/index.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Copyright 2016-2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var enums = require('../../utils/enums'); -var fns = require('../../utils/fns'); -var eventTagUtils = require('../../utils/event_tag_utils'); -var projectConfig = require('../project_config'); -var attributeValidator = require('../../utils/attributes_validator'); - -var ACTIVATE_EVENT_KEY = 'campaign_activated'; -var CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'; -var ENDPOINT = 'https://logx.optimizely.com/v1/events'; -var HTTP_VERB = 'POST'; - -/** - * Get params which are used same in both conversion and impression events - * @param {Object} options.attributes Object representing user attributes and values which need to be recorded - * @param {string} options.clientEngine The client we are using: node or javascript - * @param {string} options.clientVersion The version of the client - * @param {Object} options.configObj Object representing project configuration, including datafile information and mappings for quick lookup - * @param {string} options.userId ID for user - * @param {Object} options.Logger logger - * @return {Object} Common params with properties that are used in both conversion and impression events - */ -function getCommonEventParams(options) { - var attributes = options.attributes; - var configObj = options.configObj; - var anonymize_ip = configObj.anonymizeIP; - var botFiltering = configObj.botFiltering; - if (anonymize_ip === null || anonymize_ip === undefined) { - anonymize_ip = false; - } - - var visitor = { - snapshots: [], - visitor_id: options.userId, - attributes: [] - }; - - var commonParams = { - account_id: configObj.accountId, - project_id: configObj.projectId, - visitors: [visitor], - revision: configObj.revision, - client_name: options.clientEngine, - client_version: options.clientVersion, - anonymize_ip: anonymize_ip, - enrich_decisions: true, - }; - - // Omit attribute values that are not supported by the log endpoint. - fns.forOwn(attributes, function(attributeValue, attributeKey) { - if (attributeValidator.isAttributeValid(attributeKey, attributeValue)) { - var attributeId = projectConfig.getAttributeId(options.configObj, attributeKey, options.logger); - if (attributeId) { - commonParams.visitors[0].attributes.push({ - entity_id: attributeId, - key: attributeKey, - type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, - value: attributes[attributeKey], - }); - } - } - }); - - if (typeof botFiltering === 'boolean') { - commonParams.visitors[0].attributes.push({ - entity_id: enums.CONTROL_ATTRIBUTES.BOT_FILTERING, - key: enums.CONTROL_ATTRIBUTES.BOT_FILTERING, - type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, - value: botFiltering, - }); - } - return commonParams; -} - -/** - * Creates object of params specific to impression events - * @param {Object} configObj Object representing project configuration - * @param {string} experimentId ID of experiment for which impression needs to be recorded - * @param {string} variationId ID for variation which would be presented to user - * @return {Object} Impression event params - */ -function getImpressionEventParams(configObj, experimentId, variationId) { - var impressionEventParams = { - decisions: [{ - campaign_id: projectConfig.getLayerId(configObj, experimentId), - experiment_id: experimentId, - variation_id: variationId, - }], - events: [{ - entity_id: projectConfig.getLayerId(configObj, experimentId), - timestamp: fns.currentTimestamp(), - key: ACTIVATE_EVENT_KEY, - uuid: fns.uuid(), - }] - - }; - return impressionEventParams; -} - -/** - * Creates object of params specific to conversion events - * @param {Object} configObj Object representing project configuration - * @param {string} eventKey Event key representing the event which needs to be recorded - * @param {Object} eventTags Values associated with the event. - * @param {Object} logger Logger object - * @return {Object} Conversion event params - */ -function getVisitorSnapshot(configObj, eventKey, eventTags, logger) { - var snapshot = { - events: [] - }; - - var eventDict = { - entity_id: projectConfig.getEventId(configObj, eventKey), - timestamp: fns.currentTimestamp(), - uuid: fns.uuid(), - key: eventKey, - }; - - if (eventTags) { - var revenue = eventTagUtils.getRevenueValue(eventTags, logger); - if (revenue !== null) { - eventDict[enums.RESERVED_EVENT_KEYWORDS.REVENUE] = revenue; - } - - var eventValue = eventTagUtils.getEventValue(eventTags, logger); - if (eventValue !== null) { - eventDict[enums.RESERVED_EVENT_KEYWORDS.VALUE] = eventValue; - } - - eventDict['tags'] = eventTags; - } - snapshot.events.push(eventDict); - - return snapshot; -} - -module.exports = { - /** - * Create impression event params to be sent to the logging endpoint - * @param {Object} options Object containing values needed to build impression event - * @param {Object} options.attributes Object representing user attributes and values which need to be recorded - * @param {string} options.clientEngine The client we are using: node or javascript - * @param {string} options.clientVersion The version of the client - * @param {Object} options.configObj Object representing project configuration, including datafile information and mappings for quick lookup - * @param {string} options.experimentId Experiment for which impression needs to be recorded - * @param {string} options.userId ID for user - * @param {string} options.variationId ID for variation which would be presented to user - * @return {Object} Params to be used in impression event logging endpoint call - */ - getImpressionEvent: function(options) { - var impressionEvent = { - httpVerb: HTTP_VERB - }; - - var commonParams = getCommonEventParams(options); - impressionEvent.url = ENDPOINT; - - var impressionEventParams = getImpressionEventParams(options.configObj, options.experimentId, options.variationId); - // combine Event params into visitor obj - commonParams.visitors[0].snapshots.push(impressionEventParams); - - impressionEvent.params = commonParams; - - return impressionEvent; - }, - - /** - * Create conversion event params to be sent to the logging endpoint - * @param {Object} options Object containing values needed to build conversion event - * @param {Object} options.attributes Object representing user attributes and values which need to be recorded - * @param {string} options.clientEngine The client we are using: node or javascript - * @param {string} options.clientVersion The version of the client - * @param {Object} options.configObj Object representing project configuration, including datafile information and mappings for quick lookup - * @param {string} options.eventKey Event key representing the event which needs to be recorded - * @param {Object} options.eventTags Object with event-specific tags - * @param {Object} options.logger Logger object - * @param {string} options.userId ID for user - * @return {Object} Params to be used in conversion event logging endpoint call - */ - getConversionEvent: function(options) { - var conversionEvent = { - httpVerb: HTTP_VERB, - }; - - var commonParams = getCommonEventParams(options); - conversionEvent.url = ENDPOINT; - - var snapshot = getVisitorSnapshot(options.configObj, - options.eventKey, - options.eventTags, - options.logger); - - commonParams.visitors[0].snapshots = [snapshot]; - conversionEvent.params = commonParams; - - return conversionEvent; - }, -}; diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.tests.js b/packages/optimizely-sdk/lib/core/event_builder/index.tests.js deleted file mode 100644 index e54336658..000000000 --- a/packages/optimizely-sdk/lib/core/event_builder/index.tests.js +++ /dev/null @@ -1,1402 +0,0 @@ -/** - * Copyright 2016-2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var eventBuilder = require('./index.js'); -var packageJSON = require('../../../package.json'); -var projectConfig = require('../project_config'); -var testData = require('../../tests/test_data'); - -var chai = require('chai'); -var assert = chai.assert; -var sinon = require('sinon'); -var uuid = require('uuid'); - - -describe('lib/core/event_builder', function() { - describe('APIs', function() { - - var mockLogger; - var configObj; - var clock; - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData.getTestProjectConfig()); - clock = sinon.useFakeTimers(new Date().getTime()); - sinon.stub(uuid, 'v4').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); - mockLogger = { - log: sinon.stub(), - }; - }); - - afterEach(function() { - clock.restore(); - uuid.v4.restore(); - }); - - describe('getImpressionEvent', function() { - it('should create proper params for getImpressionEvent without attributes', function() { - - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with attributes as a string value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'firefox', - 'key': 'browser_type' - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventOptions = { - attributes: {browser_type: 'firefox'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with attributes as a false value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': false, - 'key': 'browser_type' - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - } - }; - - var eventOptions = { - attributes: {browser_type: false}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with attributes as a zero value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 0, - 'key': 'browser_type' - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {browser_type: 0}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should not fill in userFeatures for getImpressionEvent when attribute is not in the datafile', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {invalid_attribute: 'sorry_not_sorry'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - logger: mockLogger, - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering enabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '572018', - 'project_id': '594001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering', - 'type': 'custom', - 'value': true - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '595008', - 'experiment_id': '595010', - 'campaign_id': '595005' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '595005', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '35', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': true, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {'$opt_user_agent': 'Chrome'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - experimentId: '595010', - variationId: '595008', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering disabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - v4ConfigObj.botFiltering = false; - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '572018', - 'project_id': '594001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering', - 'type': 'custom', - 'value': false - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '595008', - 'experiment_id': '595010', - 'campaign_id': '595005' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '595005', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '35', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': true, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {'$opt_user_agent': 'Chrome'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - experimentId: '595010', - variationId: '595008', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with typed attributes', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '323434545', - 'key': 'boolean_key', - 'type': 'custom', - 'value': true - }, { - 'entity_id': '616727838', - 'key': 'integer_key', - 'type': 'custom', - 'value': 10 - }, { - 'entity_id': '808797686', - 'key': 'double_key', - 'type': 'custom', - 'value': 3.14 - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: { - 'browser_type': 'Chrome', - 'boolean_key': true, - 'integer_key': 10, - 'double_key': 3.14, - }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should remove invalid params from impression event payload', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '808797687', - 'key': 'valid_positive_number', - 'type': 'custom', - 'value': Math.pow(2, 53) - }, { - 'entity_id': '808797688', - 'key': 'valid_negative_number', - 'type': 'custom', - 'value': -Math.pow(2, 53) - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '4' - }], - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '4', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: { - 'browser_type': 'Chrome', - 'valid_positive_number': Math.pow(2, 53), - 'valid_negative_number': -Math.pow(2, 53), - 'invalid_number': Math.pow(2, 53) + 2, - 'array': [1, 2, 3], - }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = eventBuilder.getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - - describe('getConversionEvent', function() { - it('should create proper params for getConversionEvent without attributes or event value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'testUser', - 'attributes': [], - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getConversionEvent with attributes', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'firefox', - 'key': 'browser_type' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {browser_type: 'firefox'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getConversionEvent with event value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'revenue': 4200 - }, - 'timestamp': Math.round(new Date().getTime()), - 'revenue': 4200, - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 4200, - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getConversionEvent with attributes and event value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'firefox', - 'key': 'browser_type' - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'revenue': 4200 - }, - 'timestamp': Math.round(new Date().getTime()), - 'revenue': 4200, - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {browser_type: 'firefox'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 4200 - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should not fill in userFeatures for getConversion when attribute is not in the datafile', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {invalid_attribute: 'sorry_not_sorry'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - sinon.assert.calledOnce(mockLogger.log); - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering enabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '572018', - 'project_id': '594001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering', - 'type': 'custom', - 'value': true - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '594089', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'item_bought' - }] - }] - }], - 'revision': '35', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': true, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {'$opt_user_agent': 'Chrome'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - eventKey: 'item_bought', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering disabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - v4ConfigObj.botFiltering = false; - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '572018', - 'project_id': '594001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering', - 'type': 'custom', - 'value': false - }], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '594089', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'item_bought' - }] - }] - }], - 'revision': '35', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': true, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - attributes: {'$opt_user_agent': 'Chrome'}, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - eventKey: 'item_bought', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create the correct snapshot for multiple experiments attached to the event', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'testUser', - 'attributes': [], - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '111100', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithMultipleExperiments' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEventWithMultipleExperiments', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should remove invalid params from conversion event payload', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'Chrome' - }, { - 'entity_id': '808797687', - 'key': 'valid_positive_number', - 'type': 'custom', - 'value': Math.pow(2, 53) - }, { - 'entity_id': '808797688', - 'key': 'valid_negative_number', - 'type': 'custom', - 'value': -Math.pow(2, 53) - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '111100', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithMultipleExperiments' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEventWithMultipleExperiments', - logger: mockLogger, - userId: 'testUser', - attributes: { - 'browser_type': 'Chrome', - 'valid_positive_number': Math.pow(2, 53), - 'valid_negative_number': -Math.pow(2, 53), - 'invalid_number': -Math.pow(2, 53) - 2, - 'array': [1, 2, 3], - }, - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and event tags are passed it', function() { - it('should create proper params for getConversionEvent with event tags', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'cool', - }, - 'timestamp': Math.round(new Date().getTime()), - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and the event tags contain an entry for "revenue"', function() { - it('should include the revenue value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'cool', - 'revenue': 4200 - }, - 'timestamp': Math.round(new Date().getTime()), - 'revenue': 4200, - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'revenue': 4200, - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should include revenue value of 0 in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'revenue': 0 - }, - 'timestamp': Math.round(new Date().getTime()), - 'revenue': 0, - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'revenue': 0, - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and the revenue value is invalid', function() { - it('should not include the revenue value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'cool', - 'revenue': 'invalid revenue' - }, - 'timestamp': Math.round(new Date().getTime()), - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'revenue': 'invalid revenue', - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - }); - - describe('and the event tags contain an entry for "value"', function() { - it('should include the event value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'cool', - 'value': '13.37' - }, - 'timestamp': Math.round(new Date().getTime()), - 'value': 13.37, - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'value': '13.37', - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should include the falsy event values in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'value': '0.0' - }, - 'timestamp': Math.round(new Date().getTime()), - 'value': 0.0, - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'value': '0.0', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and the event value is invalid', function() { - it('should not include the event value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'client_version': packageJSON.version, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [], - 'visitor_id': 'testUser', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'cool', - 'value': 'invalid value' - }, - 'timestamp': Math.round(new Date().getTime()), - 'key': 'testEvent', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'node-sdk', - 'revision': '42', - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'value': 'invalid value', - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - }); - }); - - describe('createEventWithBucketingId', function () { - it('should send proper bucketingID with user attributes', function () { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '$opt_bucketing_id', - 'key': '$opt_bucketing_id', - 'type': 'custom', - 'value': 'variation', - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': Math.round(new Date().getTime()), - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent' - }] - }] - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': packageJSON.version, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - attributes: {'$opt_bucketing_id': 'variation'}, - }; - - var actualParams = eventBuilder.getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/core/notification_center/index.js b/packages/optimizely-sdk/lib/core/notification_center/index.js deleted file mode 100644 index 44025bdbc..000000000 --- a/packages/optimizely-sdk/lib/core/notification_center/index.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright 2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var enums = require('../../utils/enums'); -var fns = require('../../utils/fns'); -var sprintf = require('sprintf-js').sprintf; - -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MODULE_NAME = 'NOTIFICATION_CENTER'; - -/** - * NotificationCenter allows registration and triggering of callback functions using - * notification event types defined in NOTIFICATION_TYPES of utils/enums/index.js: - * - ACTIVATE: An impression event will be sent to Optimizely. - * - TRACK a conversion event will be sent to Optimizely - * @constructor - * @param {Object} options - * @param {Object} options.logger An instance of a logger to log messages with - * @param {object} options.errorHandler An instance of errorHandler to handle any unexpected error - * @returns {Object} - */ -function NotificationCenter(options) { - this.logger = options.logger; - this.errorHandler = options.errorHandler; - this.__notificationListeners = {}; - fns.forOwn(enums.NOTIFICATION_TYPES, function(notificationTypeEnum) { - this.__notificationListeners[notificationTypeEnum] = []; - }.bind(this)); - this.__listenerId = 1; -} - -/** - * Add a notification callback to the notification center - * @param {string} notificationType One of the values from NOTIFICATION_TYPES in utils/enums/index.js - * @param {Function} callback Function that will be called when the event is triggered - * @returns {number} If the callback was successfully added, returns a listener ID which can be used - * to remove the callback by calling removeNotificationListener. The ID is a number greater than 0. - * If there was an error and the listener was not added, addNotificationListener returns -1. This - * can happen if the first argument is not a valid notification type, or if the same callback - * function was already added as a listener by a prior call to this function. - */ -NotificationCenter.prototype.addNotificationListener = function (notificationType, callback) { - try { - var isNotificationTypeValid = fns.values(enums.NOTIFICATION_TYPES) - .indexOf(notificationType) > -1; - if (!isNotificationTypeValid) { - return -1; - } - - if (!this.__notificationListeners[notificationType]) { - this.__notificationListeners[notificationType] = []; - } - - var callbackAlreadyAdded = false; - fns.forEach(this.__notificationListeners[notificationType], function (listenerEntry) { - if (listenerEntry.callback === callback) { - callbackAlreadyAdded = true; - return false; - } - }); - if (callbackAlreadyAdded) { - return -1; - } - - this.__notificationListeners[notificationType].push({ - id: this.__listenerId, - callback: callback, - }); - - var returnId = this.__listenerId; - this.__listenerId += 1; - return returnId; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return -1; - } -}; - -/** - * Remove a previously added notification callback - * @param {number} listenerId ID of listener to be removed - * @returns {boolean} Returns true if the listener was found and removed, and false - * otherwise. - */ -NotificationCenter.prototype.removeNotificationListener = function (listenerId) { - try { - var indexToRemove; - var typeToRemove; - fns.forOwn(this.__notificationListeners, function (listenersForType, notificationType) { - fns.forEach(listenersForType, function (listenerEntry, i) { - if (listenerEntry.id === listenerId) { - indexToRemove = i; - typeToRemove = notificationType; - return false; - } - }); - if (indexToRemove !== undefined && typeToRemove !== undefined) { - return false; - } - }); - - if (indexToRemove !== undefined && typeToRemove !== undefined) { - this.__notificationListeners[typeToRemove].splice(indexToRemove, 1); - return true; - } - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } - return false; -}; - -/** - * Removes all previously added notification listeners, for all notification types - */ -NotificationCenter.prototype.clearAllNotificationListeners = function () { - try{ - fns.forOwn(enums.NOTIFICATION_TYPES, function (notificationTypeEnum) { - this.__notificationListeners[notificationTypeEnum] = []; - }.bind(this)); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } -}; - -/** - * Remove all previously added notification listeners for the argument type - * @param {string} notificationType One of enums.NOTIFICATION_TYPES - */ -NotificationCenter.prototype.clearNotificationListeners = function (notificationType) { - try { - this.__notificationListeners[notificationType] = []; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } -}; - -/** - * Fires notifications for the argument type. All registered callbacks for this type will be - * called. The notificationData object will be passed on to callbacks called. - * @param {string} notificationType One of enums.NOTIFICATION_TYPES - * @param {Object} notificationData Will be passed to callbacks called - */ -NotificationCenter.prototype.sendNotifications = function (notificationType, notificationData) { - try { - fns.forEach(this.__notificationListeners[notificationType], function (listenerEntry) { - var callback = listenerEntry.callback; - try { - callback(notificationData); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.NOTIFICATION_LISTENER_EXCEPTION, MODULE_NAME, notificationType, ex.message)); - } - }.bind(this)); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - } -}; - -module.exports = { - /** - * Create an instance of NotificationCenter - * @param {Object} options - * @param {Object} options.logger An instance of a logger to log messages with - * @returns {Object} An instance of NotificationCenter - */ - createNotificationCenter: function(options) { - return new NotificationCenter(options); - }, -}; diff --git a/packages/optimizely-sdk/lib/core/project_config/index.js b/packages/optimizely-sdk/lib/core/project_config/index.js deleted file mode 100644 index f4f0408ba..000000000 --- a/packages/optimizely-sdk/lib/core/project_config/index.js +++ /dev/null @@ -1,597 +0,0 @@ -/** - * Copyright 2016-2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var fns = require('../../utils/fns'); -var enums = require('../../utils/enums'); -var sprintf = require('sprintf-js').sprintf; -var stringValidator = require('../../utils/string_value_validator'); - -var EXPERIMENT_LAUNCHED_STATUS = 'Launched'; -var EXPERIMENT_RUNNING_STATUS = 'Running'; -var RESERVED_ATTRIBUTE_PREFIX = '$opt_'; -var MODULE_NAME = 'PROJECT_CONFIG'; - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var LOG_LEVEL = enums.LOG_LEVEL; -var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; - -module.exports = { - /** - * Creates projectConfig object to be used for quick project property lookup - * @param {Object} datafile JSON datafile representing the project - * @return {Object} Object representing project configuration - */ - createProjectConfig: function(datafile) { - var projectConfig = fns.cloneDeep(datafile); - - /* - * Conditions of audiences in projectConfig.typedAudiences are not - * expected to be string-encoded as they are here in projectConfig.audiences. - */ - fns.forEach(projectConfig.audiences, function(audience) { - audience.conditions = JSON.parse(audience.conditions); - }); - projectConfig.audiencesById = fns.keyBy(projectConfig.audiences, 'id'); - fns.assign(projectConfig.audiencesById, fns.keyBy(projectConfig.typedAudiences, 'id')); - - projectConfig.attributeKeyMap = fns.keyBy(projectConfig.attributes, 'key'); - projectConfig.eventKeyMap = fns.keyBy(projectConfig.events, 'key'); - projectConfig.groupIdMap = fns.keyBy(projectConfig.groups, 'id'); - - var experiments; - fns.forEach(projectConfig.groupIdMap, function(group, Id) { - experiments = fns.cloneDeep(group.experiments); - fns.forEach(experiments, function(experiment) { - projectConfig.experiments.push(fns.assignIn(experiment, {groupId: Id})); - }); - }); - - projectConfig.rolloutIdMap = fns.keyBy(projectConfig.rollouts || [], 'id'); - fns.forOwn(projectConfig.rolloutIdMap, function(rollout) { - fns.forEach(rollout.experiments || [], function(experiment) { - projectConfig.experiments.push(fns.cloneDeep(experiment)); - // Creates { <variationKey>: <variation> } map inside of the experiment - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); - }); - - projectConfig.experimentKeyMap = fns.keyBy(projectConfig.experiments, 'key'); - projectConfig.experimentIdMap = fns.keyBy(projectConfig.experiments, 'id'); - - projectConfig.variationIdMap = {}; - projectConfig.variationVariableUsageMap = {}; - fns.forEach(projectConfig.experiments, function(experiment) { - // Creates { <variationKey>: <variation> } map inside of the experiment - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - - // Creates { <variationId>: { key: <variationKey>, id: <variationId> } } mapping for quick lookup - fns.assignIn(projectConfig.variationIdMap, fns.keyBy(experiment.variations, 'id')); - - fns.forOwn(experiment.variationKeyMap, function(variation) { - if (variation.variables) { - projectConfig.variationVariableUsageMap[variation.id] = fns.keyBy(variation.variables, 'id'); - } - }); - }); - - projectConfig.forcedVariationMap = {}; - - projectConfig.featureKeyMap = fns.keyBy(projectConfig.featureFlags || [], 'key'); - fns.forOwn(projectConfig.featureKeyMap, function(feature) { - feature.variableKeyMap = fns.keyBy(feature.variables, 'key'); - fns.forEach(feature.experimentIds || [], function(experimentId) { - var experimentInFeature = projectConfig.experimentIdMap[experimentId]; - if (experimentInFeature.groupId) { - feature.groupId = experimentInFeature.groupId; - // Experiments in feature can only belong to one mutex group. - return false; - } - }); - }); - - return projectConfig; - }, - - /** - * Get experiment ID for the provided experiment key - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Experiment key for which ID is to be determined - * @return {string} Experiment ID corresponding to the provided experiment key - * @throws If experiment key is not in datafile - */ - getExperimentId: function(projectConfig, experimentKey) { - var experiment = projectConfig.experimentKeyMap[experimentKey]; - if (fns.isEmpty(experiment)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); - } - return experiment.id; - }, - - /** - * Get layer ID for the provided experiment key - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentId Experiment ID for which layer ID is to be determined - * @return {string} Layer ID corresponding to the provided experiment key - * @throws If experiment key is not in datafile - */ - getLayerId: function(projectConfig, experimentId) { - var experiment = projectConfig.experimentIdMap[experimentId]; - if (fns.isEmpty(experiment)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, MODULE_NAME, experimentId)); - } - return experiment.layerId; - }, - - /** - * Get attribute ID for the provided attribute key - * @param {Object} projectConfig Object representing project configuration - * @param {string} attributeKey Attribute key for which ID is to be determined - * @param {Object} logger - * @return {string|null} Attribute ID corresponding to the provided attribute key. Attribute key if it is a reserved attribute. - */ - getAttributeId: function(projectConfig, attributeKey, logger) { - var attribute = projectConfig.attributeKeyMap[attributeKey]; - var hasReservedPrefix = attributeKey.indexOf(RESERVED_ATTRIBUTE_PREFIX) === 0; - if (attribute) { - if (hasReservedPrefix) { - logger.log(LOG_LEVEL.WARN, - sprintf('Attribute %s unexpectedly has reserved prefix %s; using attribute ID instead of reserved attribute name.', attributeKey, RESERVED_ATTRIBUTE_PREFIX)); - } - return attribute.id; - } else if (hasReservedPrefix) { - return attributeKey; - } - - logger.log(LOG_LEVEL.DEBUG, sprintf(ERROR_MESSAGES.UNRECOGNIZED_ATTRIBUTE, MODULE_NAME, attributeKey)); - return null; - }, - - /** - * Get event ID for the provided - * @param {Object} projectConfig Object representing project configuration - * @param {string} eventKey Event key for which ID is to be determined - * @return {string|null} Event ID corresponding to the provided event key - */ - getEventId: function(projectConfig, eventKey) { - var event = projectConfig.eventKeyMap[eventKey]; - if (event) { - return event.id; - } - return null; - }, - - /** - * Get experiment status for the provided experiment key - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Experiment key for which status is to be determined - * @return {string} Experiment status corresponding to the provided experiment key - * @throws If experiment key is not in datafile - */ - getExperimentStatus: function(projectConfig, experimentKey) { - var experiment = projectConfig.experimentKeyMap[experimentKey]; - if (fns.isEmpty(experiment)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); - } - return experiment.status; - }, - - /** - * Returns whether experiment has a status of 'Running' or 'Launched' - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Experiment key for which status is to be compared with 'Running' - * @return {Boolean} true if experiment status is set to 'Running', false otherwise - */ - isActive: function(projectConfig, experimentKey) { - return module.exports.getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS || - module.exports.getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_LAUNCHED_STATUS; - }, - - /** - * Determine for given experiment if event is running, which determines whether should be dispatched or not - */ - isRunning: function(projectConfig, experimentKey) { - return module.exports.getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; - }, - - /** - * Get audience conditions for the experiment - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Experiment key for which audience conditions are to be determined - * @return {Array} Audience conditions for the experiment - can be an array of audience IDs, or a - * nested array of conditions - * Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"] - * @throws If experiment key is not in datafile - */ - getExperimentAudienceConditions: function(projectConfig, experimentKey) { - var experiment = projectConfig.experimentKeyMap[experimentKey]; - if (fns.isEmpty(experiment)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); - } - - return experiment.audienceConditions || experiment.audienceIds; - }, - - /** - * Get variation key given experiment key and variation ID - * @param {Object} projectConfig Object representing project configuration - * @param {string} variationId ID of the variation - * @return {string} Variation key or null if the variation ID is not found - */ - getVariationKeyFromId: function(projectConfig, variationId) { - if (projectConfig.variationIdMap.hasOwnProperty(variationId)) { - return projectConfig.variationIdMap[variationId].key; - } - return null; - }, - - /** - * Get the variation ID given the experiment key and variation key - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Key of the experiment the variation belongs to - * @param {string} variationKey The variation key - * @return {string} the variation ID - */ - getVariationIdFromExperimentAndVariationKey: function(projectConfig, experimentKey, variationKey) { - var experiment = projectConfig.experimentKeyMap[experimentKey]; - if (experiment.variationKeyMap.hasOwnProperty(variationKey)) { - return experiment.variationKeyMap[variationKey].id; - } - return null; - }, - - /** - * Get experiment from provided experiment key - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Event key for which experiment IDs are to be retrieved - * @return {Object} experiment - * @throws If experiment key is not in datafile - */ - getExperimentFromKey: function(projectConfig, experimentKey) { - if (projectConfig.experimentKeyMap.hasOwnProperty(experimentKey)) { - var experiment = projectConfig.experimentKeyMap[experimentKey]; - if (!!experiment) { - return experiment; - } - } - - throw new Error(sprintf(ERROR_MESSAGES.EXPERIMENT_KEY_NOT_IN_DATAFILE, MODULE_NAME, experimentKey)); - }, - - /** - * Given an experiment key, returns the traffic allocation within that experiment - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Key representing the experiment - * @return {Array<Object>} Traffic allocation for the experiment - * @throws If experiment key is not in datafile - */ - getTrafficAllocation: function(projectConfig, experimentKey) { - var experiment = projectConfig.experimentKeyMap[experimentKey]; - if (fns.isEmpty(experiment)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); - } - return experiment.trafficAllocation; - }, - - /** - * Removes forced variation for given userId and experimentKey - * @param {Object} projectConfig Object representing project configuration - * @param {string} userId String representing the user id - * @param {number} experimentId Number representing the experiment id - * @param {string} experimentKey Key representing the experiment id - * @param {Object} logger - * @throws If the user id is not valid or not in the forced variation map - */ - removeForcedVariation: function(projectConfig, userId, experimentId, experimentKey, logger) { - if (!userId) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_USER_ID, MODULE_NAME)); - } - - if (projectConfig.forcedVariationMap.hasOwnProperty(userId)) { - delete projectConfig.forcedVariationMap[userId][experimentId]; - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.VARIATION_REMOVED_FOR_USER, MODULE_NAME, experimentKey, userId)); - } else { - throw new Error(sprintf(ERROR_MESSAGES.USER_NOT_IN_FORCED_VARIATION, MODULE_NAME, userId)); - } - }, - - /** - * Sets forced variation for given userId and experimentKey - * @param {Object} projectConfig Object representing project configuration - * @param {string} userId String representing the user id - * @param {number} experimentId Number representing the experiment id - * @param {number} variationId Number representing the variation id - * @param {Object} logger - * @throws If the user id is not valid - */ - setInForcedVariationMap: function(projectConfig, userId, experimentId, variationId, logger) { - if (projectConfig.forcedVariationMap.hasOwnProperty(userId)) { - projectConfig.forcedVariationMap[userId][experimentId] = variationId; - } else { - projectConfig.forcedVariationMap[userId] = {}; - projectConfig.forcedVariationMap[userId][experimentId] = variationId; - } - - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_MAPPED_TO_FORCED_VARIATION, MODULE_NAME, variationId, experimentId, userId)); - }, - - /** - * Gets the forced variation key for the given user and experiment. - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Key for experiment. - * @param {string} userId The user Id. - * @param {Object} logger - * @return {string|null} Variation The variation which the given user and experiment should be forced into. - */ - getForcedVariation: function(projectConfig, experimentKey, userId, logger) { - var experimentToVariationMap = projectConfig.forcedVariationMap[userId]; - if (!experimentToVariationMap) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, MODULE_NAME, userId)); - return null; - } - - var experimentId; - try { - var experiment = this.getExperimentFromKey(projectConfig, experimentKey); - if (experiment.hasOwnProperty('id')) { - experimentId = experiment['id']; - } else { - // catching improperly formatted experiments - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.IMPROPERLY_FORMATTED_EXPERIMENT, MODULE_NAME, experimentKey)); - return null; - } - } catch (ex) { - // catching experiment not in datafile - logger.log(LOG_LEVEL.ERROR, ex.message); - return null; - } - - var variationId = experimentToVariationMap[experimentId]; - if (!variationId) { - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT, MODULE_NAME, experimentKey, userId)); - return null; - } - - var variationKey = this.getVariationKeyFromId(projectConfig, variationId); - logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_FORCED_VARIATION, MODULE_NAME, variationKey, experimentKey, userId)); - - return variationKey; - }, - - /** - * Sets the forced variation for a user in a given experiment - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentKey Key for experiment. - * @param {string} userId The user Id. - * @param {string} variationKey Key for variation. If null, then clear the existing experiment-to-variation mapping - * @param {Object} logger - * @return {boolean} A boolean value that indicates if the set completed successfully. - */ - setForcedVariation: function(projectConfig, experimentKey, userId, variationKey, logger) { - if (variationKey != null && !stringValidator.validate(variationKey)) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.INVALID_VARIATION_KEY, MODULE_NAME)); - return false; - } - - var experimentId; - try { - var experiment = this.getExperimentFromKey(projectConfig, experimentKey); - if (experiment.hasOwnProperty('id')) { - experimentId = experiment['id']; - } else { - // catching improperly formatted experiments - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.IMPROPERLY_FORMATTED_EXPERIMENT, MODULE_NAME, experimentKey)); - return false; - } - } catch (ex) { - // catching experiment not in datafile - logger.log(LOG_LEVEL.ERROR, ex.message); - return false; - } - - if (variationKey == null) { - try { - this.removeForcedVariation(projectConfig, userId, experimentId, experimentKey, logger); - return true; - } catch (ex) { - logger.log(LOG_LEVEL.ERROR, ex.message); - return false; - } - } - - var variationId = this.getVariationIdFromExperimentAndVariationKey(projectConfig, experimentKey, variationKey); - - if (!variationId) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.NO_VARIATION_FOR_EXPERIMENT_KEY, MODULE_NAME, variationKey, experimentKey)); - return false; - } - - try { - this.setInForcedVariationMap(projectConfig, userId, experimentId, variationId, logger); - return true; - } catch (ex) { - logger.log(LOG_LEVEL.ERROR, ex.message); - return false; - } - }, - - /** - * Get experiment from provided experiment id. Log an error if no experiment - * exists in the project config with the given ID. - * @param {Object} projectConfig Object representing project configuration - * @param {string} experimentId ID of desired experiment object - * @return {Object} Experiment object - */ - getExperimentFromId: function(projectConfig, experimentId, logger) { - if (projectConfig.experimentIdMap.hasOwnProperty(experimentId)) { - var experiment = projectConfig.experimentIdMap[experimentId]; - if (!!experiment) { - return experiment; - } - } - - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, MODULE_NAME, experimentId)); - return null; - }, - - /** - * Get feature from provided feature key. Log an error if no feature exists in - * the project config with the given key. - * @param {Object} projectConfig - * @param {string} featureKey - * @param {Object} logger - * @return {Object|null} Feature object, or null if no feature with the given - * key exists - */ - getFeatureFromKey: function(projectConfig, featureKey, logger) { - if (projectConfig.featureKeyMap.hasOwnProperty(featureKey)) { - var feature = projectConfig.featureKeyMap[featureKey]; - if (!!feature) { - return feature; - } - } - - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.FEATURE_NOT_IN_DATAFILE, MODULE_NAME, featureKey)); - return null; - }, - - /** - * Get the variable with the given key associated with the feature with the - * given key. If the feature key or the variable key are invalid, log an error - * message. - * @param {Object} projectConfig - * @param {string} featureKey - * @param {string} variableKey - * @param {Object} logger - * @return {Object|null} Variable object, or null one or both of the given - * feature and variable keys are invalid - */ - getVariableForFeature: function(projectConfig, featureKey, variableKey, logger) { - var feature = projectConfig.featureKeyMap[featureKey]; - if (!feature) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.FEATURE_NOT_IN_DATAFILE, MODULE_NAME, featureKey)); - return null; - } - - var variable = feature.variableKeyMap[variableKey]; - if (!variable) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.VARIABLE_KEY_NOT_IN_DATAFILE, MODULE_NAME, variableKey, featureKey)); - return null; - } - - return variable; - }, - - /** - * Get the value of the given variable for the given variation. If the given - * variable has no value for the given variation, return the variable's - * default value. Log an error message if the variation is invalid. If the - * variable or variation are invalid, return null. - * @param {Object} projectConfig - * @param {Object} variable - * @param {Object} variation - * @param {Object} logger - * @return {string|null} The value of the given variable for the given - * variation, or the variable default value if the given variable has no value - * for the given variation, or null if the variation or variable are invalid - */ - getVariableValueForVariation: function(projectConfig, variable, variation, logger) { - if (!variable || !variation) { - return null; - } - - if (!projectConfig.variationVariableUsageMap.hasOwnProperty(variation.id)) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.VARIATION_ID_NOT_IN_DATAFILE_NO_EXPERIMENT, MODULE_NAME, variation.id)); - return null; - } - - var variableUsages = projectConfig.variationVariableUsageMap[variation.id]; - var variableUsage = variableUsages[variable.id]; - return variableUsage ? variableUsage.value : variable.defaultValue; - }, - - /** - * Given a variable value in string form, try to cast it to the argument type. - * If the type cast succeeds, return the type casted value, otherwise log an - * error and return null. - * @param {string} variableValue Variable value in string form - * @param {string} variableType Type of the variable whose value was passed - * in the first argument. Must be one of - * FEATURE_VARIABLE_TYPES in - * lib/utils/enums/index.js. The return value's - * type is determined by this argument (boolean - * for BOOLEAN, number for INTEGER or DOUBLE, - * and string for STRING). - * @param {Object} logger Logger instance - * @returns {*} Variable value of the appropriate type, or - * null if the type cast failed - */ - getTypeCastValue: function(variableValue, variableType, logger) { - var castValue; - - switch (variableType) { - case FEATURE_VARIABLE_TYPES.BOOLEAN: - if (variableValue !== 'true' && variableValue !== 'false') { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME, variableValue, variableType)); - castValue = null; - } else { - castValue = variableValue === 'true'; - } - break; - - case FEATURE_VARIABLE_TYPES.INTEGER: - castValue = parseInt(variableValue, 10); - if (isNaN(castValue)) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME, variableValue, variableType)); - castValue = null; - } - break; - - case FEATURE_VARIABLE_TYPES.DOUBLE: - castValue = parseFloat(variableValue); - if (isNaN(castValue)) { - logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME, variableValue, variableType)); - castValue = null; - } - break; - - default: // type is STRING - castValue = variableValue; - break; - } - - return castValue; - }, - - /** - * Returns an object containing all audiences in the project config. Keys are audience IDs - * and values are audience objects. - * @param projectConfig - * @returns {Object} - */ - getAudiencesById: function(projectConfig) { - return projectConfig.audiencesById; - }, - - /** - * Returns true if an event with the given key exists in the datafile, and false otherwise - * @param {Object} projectConfig - * @param {string} eventKey - * @returns {boolean} - */ - eventWithKeyExists: function(projectConfig, eventKey) { - return projectConfig.eventKeyMap.hasOwnProperty(eventKey); - }, -}; diff --git a/packages/optimizely-sdk/lib/core/project_config/index.tests.js b/packages/optimizely-sdk/lib/core/project_config/index.tests.js deleted file mode 100644 index 15c8942fb..000000000 --- a/packages/optimizely-sdk/lib/core/project_config/index.tests.js +++ /dev/null @@ -1,730 +0,0 @@ -/** - * Copyright 2016-2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var projectConfig = require('./'); -var enums = require('../../utils/enums'); -var testDatafile = require('../../tests/test_data'); - -var _ = require('lodash/core'); -var fns = require('../../utils/fns'); -var chai = require('chai'); -var assert = chai.assert; -var logger = require('../../plugins/logger'); -var sinon = require('sinon'); -var sprintf = require('sprintf-js').sprintf; - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; -var LOG_LEVEL = enums.LOG_LEVEL; - -describe('lib/core/project_config', function() { - var parsedAudiences = testDatafile.getParsedAudiences; - describe('createProjectConfig method', function() { - it('should set properties correctly when createProjectConfig is called', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - _.forEach(testData.audiences, function(audience) { - audience.conditions = JSON.parse(audience.conditions); - }); - - assert.strictEqual(configObj.accountId, testData.accountId); - assert.strictEqual(configObj.projectId, testData.projectId); - assert.strictEqual(configObj.revision, testData.revision); - assert.deepEqual(configObj.events, testData.events); - assert.deepEqual(configObj.audiences, testData.audiences); - assert.deepEqual(configObj.groups, testData.groups); - - var expectedGroupIdMap = { - 666: testData.groups[0], - 667: testData.groups[1], - }; - - assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap); - - var expectedExperiments = testData.experiments; - _.forEach(configObj.groupIdMap, function(group, Id) { - _.forEach(group.experiments, function(experiment) { - experiment.groupId = Id; - expectedExperiments.push(experiment); - }); - }); - - _.forEach(expectedExperiments, function(experiment) { - experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); - }); - - assert.deepEqual(configObj.experiments, expectedExperiments); - - var expectedAttributeKeyMap = { - browser_type: testData.attributes[0], - boolean_key: testData.attributes[1], - integer_key: testData.attributes[2], - double_key: testData.attributes[3], - valid_positive_number: testData.attributes[4], - valid_negative_number: testData.attributes[5], - invalid_number: testData.attributes[6], - array: testData.attributes[7], - }; - - assert.deepEqual(configObj.attributeKeyMap, expectedAttributeKeyMap); - - var expectedExperimentKeyMap = { - testExperiment: configObj.experiments[0], - testExperimentWithAudiences: configObj.experiments[1], - testExperimentNotRunning: configObj.experiments[2], - testExperimentLaunched: configObj.experiments[3], - groupExperiment1: configObj.experiments[4], - groupExperiment2: configObj.experiments[5], - overlappingGroupExperiment1: configObj.experiments[6], - }; - - assert.deepEqual(configObj.experimentKeyMap, expectedExperimentKeyMap); - - var expectedEventKeyMap = { - testEvent: testData.events[0], - 'Total Revenue': testData.events[1], - testEventWithAudiences: testData.events[2], - testEventWithoutExperiments: testData.events[3], - testEventWithExperimentNotRunning: testData.events[4], - testEventWithMultipleExperiments: testData.events[5], - testEventLaunched: testData.events[6], - }; - - assert.deepEqual(configObj.eventKeyMap, expectedEventKeyMap); - - var expectedExperimentIdMap = { - '111127': configObj.experiments[0], - '122227': configObj.experiments[1], - '133337': configObj.experiments[2], - '144447': configObj.experiments[3], - '442': configObj.experiments[4], - '443': configObj.experiments[5], - '444': configObj.experiments[6], - }; - - assert.deepEqual(configObj.experimentIdMap, expectedExperimentIdMap); - - var expectedVariationKeyMap = {}; - expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[0].key] = testData.experiments[0].variations[0]; - expectedVariationKeyMap[testData.experiments[0].key + testData.experiments[0].variations[1].key] = testData.experiments[0].variations[1]; - expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[0].key] = testData.experiments[1].variations[0]; - expectedVariationKeyMap[testData.experiments[1].key + testData.experiments[1].variations[1].key] = testData.experiments[1].variations[1]; - expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[0].key] = testData.experiments[2].variations[0]; - expectedVariationKeyMap[testData.experiments[2].key + testData.experiments[2].variations[1].key] = testData.experiments[2].variations[1]; - expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[0].key] = configObj.experiments[3].variations[0]; - expectedVariationKeyMap[configObj.experiments[3].key + configObj.experiments[3].variations[1].key] = configObj.experiments[3].variations[1]; - expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[0].key] = configObj.experiments[4].variations[0]; - expectedVariationKeyMap[configObj.experiments[4].key + configObj.experiments[4].variations[1].key] = configObj.experiments[4].variations[1]; - expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[0].key] = configObj.experiments[5].variations[0]; - expectedVariationKeyMap[configObj.experiments[5].key + configObj.experiments[5].variations[1].key] = configObj.experiments[5].variations[1]; - expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[0].key] = configObj.experiments[6].variations[0]; - expectedVariationKeyMap[configObj.experiments[6].key + configObj.experiments[6].variations[1].key] = configObj.experiments[6].variations[1]; - - var expectedVariationIdMap = { - '111128': testData.experiments[0].variations[0], - '111129': testData.experiments[0].variations[1], - '122228': testData.experiments[1].variations[0], - '122229': testData.experiments[1].variations[1], - '133338': testData.experiments[2].variations[0], - '133339': testData.experiments[2].variations[1], - '144448': testData.experiments[3].variations[0], - '144449': testData.experiments[3].variations[1], - '551': configObj.experiments[4].variations[0], - '552': configObj.experiments[4].variations[1], - '661': configObj.experiments[5].variations[0], - '662': configObj.experiments[5].variations[1], - '553': configObj.experiments[6].variations[0], - '554': configObj.experiments[6].variations[1], - }; - - assert.deepEqual(configObj.variationIdMap, expectedVariationIdMap); - }); - - describe('feature management', function() { - var configObj; - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - }); - - it('creates a rolloutIdMap from rollouts in the datafile', function() { - assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); - }); - - it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() { - assert.deepEqual(configObj.variationVariableUsageMap, testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap); - }); - - it('creates a featureKeyMap from feature flags in the datafile', function() { - assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); - }); - - it('adds variations from rollout experiments to variationIdMap', function() { - assert.deepEqual(configObj.variationIdMap['594032'], { - 'variables': [ - { 'value': 'true', 'id': '4919852825313280' }, - { 'value': '395', 'id': '5482802778734592' }, - { 'value': '4.99', 'id': '6045752732155904' }, - { 'value': 'Hello audience', 'id': '6327227708866560' } - ], - 'featureEnabled': true, - 'key': '594032', - 'id': '594032' - }); - assert.deepEqual(configObj.variationIdMap['594038'], { - 'variables': [ - { 'value': 'false', 'id': '4919852825313280' }, - { 'value': '400', 'id': '5482802778734592' }, - { 'value': '14.99', 'id': '6045752732155904' }, - { 'value': 'Hello', 'id': '6327227708866560' }, - ], - 'featureEnabled': false, - 'key': '594038', - 'id': '594038' - }); - assert.deepEqual(configObj.variationIdMap['594061'], { - 'variables': [ - { 'value': '27.34', 'id': '5060590313668608' }, - { 'value': 'Winter is NOT coming', 'id': '5342065290379264' }, - { 'value': '10003', 'id': '6186490220511232' }, - { 'value': 'false', 'id': '6467965197221888' }, - ], - 'featureEnabled': true, - 'key': '594061', - 'id': '594061' - }); - assert.deepEqual(configObj.variationIdMap['594067'], { - 'variables': [ - { 'value': '30.34', 'id': '5060590313668608' }, - { 'value': 'Winter is coming definitely', 'id': '5342065290379264' }, - { 'value': '500', 'id': '6186490220511232' }, - { 'value': 'true', 'id': '6467965197221888' } - ], - 'featureEnabled': true, - 'key': '594067', - 'id': '594067', - }); - }); - }); - }); - - describe('projectConfig helper methods', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj; - var createdLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData); - sinon.stub(createdLogger, 'log'); - }); - - afterEach(function() { - createdLogger.log.restore(); - }); - - it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { - assert.strictEqual(projectConfig.getExperimentId(configObj, testData.experiments[0].key), - testData.experiments[0].id); - }); - - it('should throw error for invalid experiment key in getExperimentId', function() { - assert.throws(function() { - projectConfig.getExperimentId(configObj, 'invalidExperimentKey'); - }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey')); - }); - - it('should retrieve layer ID for valid experiment key in getLayerId', function() { - assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4'); - }); - - it('should throw error for invalid experiment key in getLayerId', function() { - assert.throws(function() { - projectConfig.getLayerId(configObj, 'invalidExperimentKey'); - }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey')); - }); - - it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { - assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094'); - }); - - it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { - assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent'); - }); - - it('should return null for invalid attribute key in getAttributeId', function() { - assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)); - sinon.assert.calledWithExactly(createdLogger.log, - LOG_LEVEL.DEBUG, - 'PROJECT_CONFIG: Unrecognized attribute invalidAttributeKey provided. Pruning before sending event to Optimizely.'); - }); - - it('should return null for invalid attribute key in getAttributeId', function() { - // Adding attribute in key map with reserved prefix - configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { - id: '42', - key: '$opt_some_reserved_attribute' - }; - assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_some_reserved_attribute', createdLogger), '42'); - sinon.assert.calledWithExactly(createdLogger.log, - LOG_LEVEL.WARN, - 'Attribute $opt_some_reserved_attribute unexpectedly has reserved prefix $opt_; using attribute ID instead of reserved attribute name.'); - }); - - it('should retrieve event ID for valid event key in getEventId', function() { - assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095'); - }); - - it('should return null for invalid event key in getEventId', function() { - assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey')); - }); - - it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { - assert.strictEqual(projectConfig.getExperimentStatus(configObj, testData.experiments[0].key), - testData.experiments[0].status); - }); - - it('should throw error for invalid experiment key in getExperimentStatus', function() { - assert.throws(function() { - projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey'); - }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey')); - }); - - it('should return true if experiment status is set to Running or Launch in isActive', function() { - assert.isTrue(projectConfig.isActive(configObj, 'testExperiment')); - - assert.isTrue(projectConfig.isActive(configObj, 'testExperimentLaunched')); - }); - - it('should return true if experiment status is set to Running or Launch in isActive', function() { - assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning')); - }); - - it('should return true if experiment status is set to Running in isRunning', function() { - assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment')); - }); - - it('should return false if experiment status is not set to Running in isRunning', function() { - assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched')); - }); - - it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { - assert.deepEqual(projectConfig.getVariationKeyFromId(configObj, - testData.experiments[0].variations[0].id), - testData.experiments[0].variations[0].key); - }); - - it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { - assert.deepEqual(projectConfig.getTrafficAllocation(configObj, testData.experiments[0].key), - testData.experiments[0].trafficAllocation); - }); - - it('should throw error for invalid experient key in getTrafficAllocation', function() { - assert.throws(function() { - projectConfig.getTrafficAllocation(configObj, 'invalidExperimentKey'); - }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey')); - }); - - describe('#getVariationIdFromExperimentAndVariationKey', function() { - it('should return the variation id for the given experiment key and variation key', function() { - assert.strictEqual( - projectConfig.getVariationIdFromExperimentAndVariationKey( - configObj, - testData.experiments[0].key, - testData.experiments[0].variations[0].key - ), - testData.experiments[0].variations[0].id - ); - }); - }); - - describe('feature management', function() { - var featureManagementLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); - sinon.stub(featureManagementLogger, 'log'); - }); - - afterEach(function() { - featureManagementLogger.log.restore(); - }); - - describe('getVariableForFeature', function() { - it('should return a variable object for a valid variable and feature key', function() { - var featureKey = 'test_feature_for_experiment'; - var variableKey = 'num_buttons'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.deepEqual(result, { - type: 'integer', - key: 'num_buttons', - id: '4792309476491264', - defaultValue: '10', - }); - }); - - it('should return null for an invalid variable key and a valid feature key', function() { - var featureKey = 'test_feature_for_experiment'; - var variableKey = 'notARealVariable____'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.log); - sinon.assert.calledWithExactly(featureManagementLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Variable with key "notARealVariable____" associated with feature with key "test_feature_for_experiment" is not in datafile.'); - }); - - it('should return null for an invalid feature key', function() { - var featureKey = 'notARealFeature_____'; - var variableKey = 'num_buttons'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.log); - sinon.assert.calledWithExactly(featureManagementLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Feature key notARealFeature_____ is not in datafile.'); - }); - - it('should return null for an invalid variable key and an invalid feature key', function() { - var featureKey = 'notARealFeature_____'; - var variableKey = 'notARealVariable____'; - var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledOnce(featureManagementLogger.log); - sinon.assert.calledWithExactly(featureManagementLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Feature key notARealFeature_____ is not in datafile.'); - }); - }); - - describe('getVariableValueForVariation', function() { - it('returns a value for a valid variation and variable', function() { - var variation = configObj.variationIdMap['594096']; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, '2'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.is_button_animated; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, 'true'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_txt; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, 'Buy me NOW'); - - variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.button_width; - result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, '20.25'); - }); - - it('returns null for a null variation', function() { - var variation = null; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, null); - }); - - it('returns null for a null variable', function() { - var variation = configObj.variationIdMap['594096']; - var variable = null; - var result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, null); - }); - - it('returns null for a null variation and null variable', function() { - var variation = null; - var variable = null; - var result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, null); - }); - - it('returns null for a variation whose id is not in the datafile', function() { - var variation = { - key: 'some_variation', - id: '999999999999', - variables: [], - }; - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, null); - }); - - it('returns the variable default value if the variation does not have a value for this variable', function() { - var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it - var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; - var result = projectConfig.getVariableValueForVariation(configObj, variable, variation, featureManagementLogger); - assert.strictEqual(result, '10'); - }); - }); - - describe('getTypeCastValue', function() { - it('can cast a boolean', function() { - var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, true); - result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, false); - }); - - it('can cast an integer', function() { - var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, 50); - var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, -7); - var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, 0); - }); - - it('can cast a double', function() { - var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 89.99); - var result = projectConfig.getTypeCastValue('-257.21', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, -257.21); - var result = projectConfig.getTypeCastValue('0', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 0); - var result = projectConfig.getTypeCastValue('10', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, 10); - }); - - it('can return a string unmodified', function() { - var result = projectConfig.getTypeCastValue('message', FEATURE_VARIABLE_TYPES.STRING, featureManagementLogger); - assert.strictEqual(result, 'message'); - }); - - it('returns null and logs an error for an invalid boolean', function() { - var result = projectConfig.getTypeCastValue('notabool', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledWithExactly(featureManagementLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Unable to cast value notabool to type boolean, returning null.'); - }); - - it('returns null and logs an error for an invalid integer', function() { - var result = projectConfig.getTypeCastValue('notanint', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledWithExactly(featureManagementLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Unable to cast value notanint to type integer, returning null.'); - }); - - it('returns null and logs an error for an invalid double', function() { - var result = projectConfig.getTypeCastValue('notadouble', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); - assert.strictEqual(result, null); - sinon.assert.calledWithExactly(featureManagementLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Unable to cast value notadouble to type double, returning null.'); - }); - }); - }); - - describe('#getAudiencesById', function() { - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - }); - - it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { - assert.deepEqual( - projectConfig.getAudiencesById(configObj), - testDatafile.typedAudiencesById - ); - }); - }); - - describe('#getExperimentAudienceConditions', function() { - it('should retrieve audiences for valid experiment key', function() { - configObj = projectConfig.createProjectConfig(testData); - assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].key), - ['11154']); - }); - - it('should throw error for invalid experiment key', function() { - configObj = projectConfig.createProjectConfig(testData); - assert.throws(function() { - projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentKey'); - }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey')); - }); - - it('should return experiment audienceIds if experiment has no audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - var result = projectConfig.getExperimentAudienceConditions(configObj, 'feat_with_var_test'); - assert.deepEqual(result, ['3468206642', '3988293898', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']); - }); - - it('should return experiment audienceConditions if experiment has audienceConditions', function() { - configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); - // audience_combinations_experiment has both audienceConditions and audienceIds - // audienceConditions should be preferred over audienceIds - var result = projectConfig.getExperimentAudienceConditions(configObj, 'audience_combinations_experiment'); - assert.deepEqual(result, ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']]); - }); - }); - }); - - describe('#getForcedVariation', function() { - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - - it('should return null for valid experimentKey, not set', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - assert.strictEqual(variation, null); - }); - - it('should return null for invalid experimentKey, not set', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var variation = projectConfig.getForcedVariation(configObj, 'definitely_not_valid_exp_key', 'user1', createdLogger); - assert.strictEqual(variation, null); - }); - }); - - describe('#setForcedVariation', function() { - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - - it('should return true for a valid forcedVariation in setForcedVariation', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - assert.strictEqual(didSetVariation, true); - }); - - it('should return the same variation from getVariation as was set in setVariation', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - assert.strictEqual(variation, 'control'); - }); - - it('should not set for an invalid variation key', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'definitely_not_valid_variation_key', createdLogger); - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - assert.strictEqual(variation, null); - }); - - it('should reset the forcedVariation if passed null', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - assert.strictEqual(didSetVariation, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - assert.strictEqual(variation, 'control'); - - var didSetVariationAgain = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', null, createdLogger); - assert.strictEqual(didSetVariationAgain, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - assert.strictEqual(variation, null); - }); - - it('should be able to add variations for multiple experiments for one user', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = projectConfig.setForcedVariation(configObj, 'testExperimentLaunched', 'user1', 'controlLaunched', createdLogger); - assert.strictEqual(didSetVariation2, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - var variation2 = projectConfig.getForcedVariation(configObj, 'testExperimentLaunched', 'user1', createdLogger); - - assert.strictEqual(variation, 'control'); - assert.strictEqual(variation2, 'controlLaunched'); - }); - - it('should be able to add experiments for multiple users', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - assert.strictEqual(didSetVariation, true); - - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user2', 'variation', createdLogger); - assert.strictEqual(didSetVariation, true); - - var variationControl = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - var variationVariation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user2', createdLogger); - - assert.strictEqual(variationControl, 'control'); - assert.strictEqual(variationVariation, 'variation'); - }); - - it('should be able to reset a variation for a user with multiple experiments', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - //set the first time - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = projectConfig.setForcedVariation(configObj, 'testExperimentLaunched', 'user1', 'controlLaunched', createdLogger); - assert.strictEqual(didSetVariation2, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - var variation2 = projectConfig.getForcedVariation(configObj, 'testExperimentLaunched', 'user1', createdLogger); - - assert.strictEqual(variation, 'control'); - assert.strictEqual(variation2, 'controlLaunched'); - - //reset for one of the experiments - var didSetVariationAgain = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'variation', createdLogger); - assert.strictEqual(didSetVariationAgain, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - var variation2 = projectConfig.getForcedVariation(configObj, 'testExperimentLaunched', 'user1', createdLogger); - - assert.strictEqual(variation, 'variation'); - assert.strictEqual(variation2, 'controlLaunched'); - }); - - it('should be able to unset a variation for a user with multiple experiments', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - //set the first time - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', 'control', createdLogger); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = projectConfig.setForcedVariation(configObj, 'testExperimentLaunched', 'user1', 'controlLaunched', createdLogger); - assert.strictEqual(didSetVariation2, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - var variation2 = projectConfig.getForcedVariation(configObj, 'testExperimentLaunched', 'user1', createdLogger); - - assert.strictEqual(variation, 'control'); - assert.strictEqual(variation2, 'controlLaunched'); - - //reset for one of the experiments - projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', null, createdLogger); - assert.strictEqual(didSetVariation, true); - - var variation = projectConfig.getForcedVariation(configObj, 'testExperiment', 'user1', createdLogger); - var variation2 = projectConfig.getForcedVariation(configObj, 'testExperimentLaunched', 'user1', createdLogger); - - assert.strictEqual(variation, null); - assert.strictEqual(variation2, 'controlLaunched'); - }); - - it('should return false for an empty variation key', function() { - var testData = testDatafile.getTestProjectConfig(); - var configObj = projectConfig.createProjectConfig(testData); - - var didSetVariation = projectConfig.setForcedVariation(configObj, 'testExperiment', 'user1', '', createdLogger); - assert.strictEqual(didSetVariation, false); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/index.browser.js b/packages/optimizely-sdk/lib/index.browser.js deleted file mode 100644 index aaf083922..000000000 --- a/packages/optimizely-sdk/lib/index.browser.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var logging = require('@optimizely/js-sdk-logging'); -var fns = require('./utils/fns'); -var configValidator = require('./utils/config_validator'); -var defaultErrorHandler = require('./plugins/error_handler'); -var defaultEventDispatcher = require('./plugins/event_dispatcher/index.browser'); -var enums = require('./utils/enums'); -var loggerPlugin = require('./plugins/logger'); -var Optimizely = require('./optimizely'); - -var logger = logging.getLogger(); -logging.setLogHandler(loggerPlugin.createLogger()); -logging.setLogLevel(logging.LogLevel.INFO); - -/** - * Entry point into the Optimizely Browser SDK - */ -module.exports = { - logging: loggerPlugin, - errorHandler: defaultErrorHandler, - eventDispatcher: defaultEventDispatcher, - enums: enums, - - setLogger: logging.setLogHandler, - setLogLevel: logging.setLogLevel, - - /** - * Creates an instance of the Optimizely class - * @param {Object} config - * @param {Object} config.datafile - * @param {Object} config.errorHandler - * @param {Object} config.eventDispatcher - * @param {Object} config.logger - * @param {Object} config.logLevel - * @param {Object} config.userProfileService - * @return {Object} the Optimizely object - */ - createInstance: function(config) { - try { - config = config || {}; - - // TODO warn about setting per instance errorHandler / logger / logLevel - if (config.errorHandler) { - logging.setErrorHandler(config.errorHandler); - } - if (config.logger) { - logging.setLogHandler(config.logger); - // respect the logger's shouldLog functionality - logging.setLogLevel(logging.LogLevel.NOTSET); - } - if (config.logLevel !== undefined) { - logging.setLogLevel(config.logLevel); - } - - try { - configValidator.validate(config); - config.isValidInstance = true; - } catch (ex) { - logger.error(ex); - config.isValidInstance = false; - } - - // Explicitly check for null or undefined - // prettier-ignore - if (config.skipJSONValidation == null) { // eslint-disable-line eqeqeq - config.skipJSONValidation = true; - } - - config = fns.assignIn( - { - eventDispatcher: defaultEventDispatcher, - }, - config, - { - clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, - // always get the OptimizelyLogger facade from logging - logger: logger, - errorHandler: logging.getErrorHandler(), - } - ); - - return new Optimizely(config); - } catch (e) { - logger.error(e); - return null; - } - }, -}; diff --git a/packages/optimizely-sdk/lib/index.browser.tests.js b/packages/optimizely-sdk/lib/index.browser.tests.js deleted file mode 100644 index 011358563..000000000 --- a/packages/optimizely-sdk/lib/index.browser.tests.js +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var logging = require('@optimizely/js-sdk-logging'); -var configValidator = require('./utils/config_validator'); -var Optimizely = require('./optimizely'); -var optimizelyFactory = require('./index.browser'); -var packageJSON = require('../package.json'); -var testData = require('./tests/test_data'); - -var chai = require('chai'); -var assert = chai.assert; -var find = require('lodash/find'); -var sinon = require('sinon'); - -describe('javascript-sdk', function() { - describe('APIs', function() { - var xhr; - var requests; - - it('should expose logger, errorHandler, eventDispatcher and enums', function() { - assert.isDefined(optimizelyFactory.logging); - assert.isDefined(optimizelyFactory.logging.createLogger); - assert.isDefined(optimizelyFactory.logging.createNoOpLogger); - assert.isDefined(optimizelyFactory.errorHandler); - assert.isDefined(optimizelyFactory.eventDispatcher); - assert.isDefined(optimizelyFactory.enums); - }); - - describe('createInstance', function() { - var fakeErrorHandler = { handleError: function() {}}; - var fakeEventDispatcher = { dispatchEvent: function() {}}; - var silentLogger; - - beforeEach(function() { - silentLogger = optimizelyFactory.logging.createLogger({ - logLevel: optimizelyFactory.enums.LOG_LEVEL.INFO, - logToConsole: false, - }); - sinon.spy(console, 'error'); - sinon.stub(configValidator, 'validate'); - - xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - requests = []; - xhr.onCreate = function (req) { - requests.push(req); - }; - }); - - afterEach(function() { - console.error.restore(); - configValidator.validate.restore(); - xhr.restore(); - }); - - it('should not throw if the provided config is not valid', function() { - configValidator.validate.throws(new Error('Invalid config or something')); - assert.doesNotThrow(function() { - optimizelyFactory.createInstance({ - datafile: {}, - logger: silentLogger, - }); - }); - }); - - it('should create an instance of optimizely', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: {}, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - - assert.instanceOf(optlyInstance, Optimizely); - assert.equal(optlyInstance.clientVersion, '3.1.0-beta1'); - }); - - it('should set the JavaScript client engine and version', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: {}, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - assert.equal('javascript-sdk', optlyInstance.clientEngine); - assert.equal(packageJSON.version, optlyInstance.clientVersion); - }); - - it('should activate with provided event dispatcher', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - var activate = optlyInstance.activate('testExperiment', 'testUser'); - assert.strictEqual(activate, 'control'); - }); - - it('should be able to set and get a forced variation', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); - assert.strictEqual(didSetVariation, true); - - var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'control'); - }); - - it('should be able to set and unset a forced variation', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); - assert.strictEqual(didSetVariation, true); - - var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'control'); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperiment', 'testUser', null); - assert.strictEqual(didSetVariation2, true); - - var variation2 = optlyInstance.getForcedVariation('testExperiment', 'testUser'); - assert.strictEqual(variation2, null); - }); - - it('should be able to set multiple experiments for one user', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'testUser', 'controlLaunched'); - assert.strictEqual(didSetVariation2, true); - - - var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'control'); - - var variation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'testUser'); - assert.strictEqual(variation2, 'controlLaunched'); - }); - - it('should be able to set multiple experiments for one user, and unset one', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'testUser', 'controlLaunched'); - assert.strictEqual(didSetVariation2, true); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'testUser', null); - assert.strictEqual(didSetVariation2, true); - - var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'control'); - - var variation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'testUser'); - assert.strictEqual(variation2, null); - }); - - it('should be able to set multiple experiments for one user, and reset one', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'testUser', 'controlLaunched'); - assert.strictEqual(didSetVariation2, true); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'testUser', 'variationLaunched'); - assert.strictEqual(didSetVariation2, true); - - var variation = optlyInstance.getForcedVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'control'); - - var variation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'testUser'); - assert.strictEqual(variation2, 'variationLaunched'); - }); - - it('should override bucketing when setForcedVariation is called', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'control'); - assert.strictEqual(didSetVariation, true); - - var variation = optlyInstance.getVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'control'); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperiment', 'testUser', 'variation'); - assert.strictEqual(didSetVariation2, true); - - var variation = optlyInstance.getVariation('testExperiment', 'testUser'); - assert.strictEqual(variation, 'variation'); - }); - - it('should override bucketing when setForcedVariation is called for a not running experiment', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - - var didSetVariation = optlyInstance.setForcedVariation('testExperimentNotRunning', 'testUser', 'controlNotRunning'); - assert.strictEqual(didSetVariation, true); - - var variation = optlyInstance.getVariation('testExperimentNotRunning', 'testUser'); - assert.strictEqual(variation, null); - }); - - describe('when passing in logLevel', function() { - beforeEach(function() { - sinon.stub(logging, 'setLogLevel'); - }); - - afterEach(function() { - logging.setLogLevel.restore(); - }); - - it('should call logging.setLogLevel', function() { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - logLevel: optimizelyFactory.enums.LOG_LEVEL.ERROR, - }); - sinon.assert.calledOnce(logging.setLogLevel); - sinon.assert.calledWithExactly(logging.setLogLevel, optimizelyFactory.enums.LOG_LEVEL.ERROR); - }); - }); - - describe('when passing in logger', function() { - beforeEach(function() { - sinon.stub(logging, 'setLogHandler'); - }); - - afterEach(function() { - logging.setLogHandler.restore(); - }); - - it('should call logging.setLogHandler with the supplied logger', function() { - var fakeLogger = { log: function() {} }; - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - logger: fakeLogger, - }); - sinon.assert.calledOnce(logging.setLogHandler); - sinon.assert.calledWithExactly(logging.setLogHandler, fakeLogger); - }); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/index.d.ts b/packages/optimizely-sdk/lib/index.d.ts deleted file mode 100644 index 5fb8a55ac..000000000 --- a/packages/optimizely-sdk/lib/index.d.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright 2018, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare module '@optimizely/optimizely-sdk' { - import enums = require('@optimizely/optimizely-sdk/lib/utils/enums'); - - export function createInstance(config: Config): Client; - - // The options object given to Optimizely.createInstance. - export interface Config { - datafile: object; - errorHandler?: object; - eventDispatcher?: object; - logger?: object; - logLevel?: enums.LOG_LEVEL.DEBUG | enums.LOG_LEVEL.ERROR | enums.LOG_LEVEL.INFO | enums.LOG_LEVEL.NOTSET | enums.LOG_LEVEL.WARNING; - skipJSONValidation?: boolean; - jsonSchemaValidator?: object; - userProfileService?: UserProfileService | null; - } - - export interface Client { - notificationCenter: NotificationCenter; - activate(experimentKey: string, userId: string, attributes?: UserAttributes): string | null; - track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void; - getVariation(experimentKey: string, userId: string, attributes?: UserAttributes): string | null; - setForcedVariation(experimentKey: string, userId: string, variationKey: string | null): boolean; - getForcedVariation(experimentKey: string, userId: string): string | null; - isFeatureEnabled(featureKey: string, userId: string, attributes?: UserAttributes): boolean; - getEnabledFeatures(userId: string, attributes?: UserAttributes): string[]; - getFeatureVariableBoolean(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): boolean | null; - getFeatureVariableDouble(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): number | null; - getFeatureVariableInteger(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): number | null; - getFeatureVariableString(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): string | null; - } - - // An event to be submitted to Optimizely, enabling tracking the reach and impact of - // tests and feature rollouts. - export interface Event { - // URL to which to send the HTTP request. - url: string, - // HTTP method with which to send the event. - httpVerb: 'POST', - // Value to send in the request body, JSON-serialized. - params: any, - } - - export interface EventDispatcher { - /** - * @param event - * Event being submitted for eventual dispatch. - * @param callback - * After the event has at least been queued for dispatch, call this function to return - * control back to the Client. - */ - dispatchEvent: (event: Event, callback: () => void) => void, - } - - export interface UserProfileService { - lookup: (userId: string) => UserProfile, - save: (profile: UserProfile) => void, - } - - // NotificationCenter-related types - export interface NotificationCenter { - addNotificationListener<T extends ListenerPayload>(notificationType: string, callback: NotificationListener<T>): number; - removeNotificationListener(listenerId: number): boolean; - clearAllNotificationListeners(): void; - clearNotificationListeners(notificationType: enums.NOTIFICATION_TYPES): void; - } - - export type NotificationListener<T extends ListenerPayload> = (notificationData: T) => void; - - export interface ListenerPayload { - userId: string; - attributes: UserAttributes; - } - - export interface ActivateListenerPayload extends ListenerPayload { - experiment: Experiment; - variation: Variation; - logEvent: Event; - } - - export type UserAttributes = { - [name: string]: string - }; - - export type EventTags = { - [key: string]: string | number | boolean, - }; - - export interface TrackListenerPayload extends ListenerPayload { - eventKey: string; - eventTags: EventTags; - logEvent: Event; - } - - interface Experiment { - id: string, - key: string, - status: string, - layerId: string, - variations: Variation[], - trafficAllocation: Array<{ - entityId: string, - endOfRange: number, - }>, - audienceIds: string[], - forcedVariations: object, - } - - interface Variation { - id: string, - key: string, - } - - export interface Logger { - log: (logLevel: enums.LOG_LEVEL, message: string) => void, - } - - // Information about past bucketing decisions for a user. - export interface UserProfile { - user_id: string, - experiment_bucket_map: { - [experiment_id: string]: { - variation_id: string, - }, - }, - } - } - - declare module '@optimizely/optimizely-sdk/lib/utils/enums'{ - export enum LOG_LEVEL{ - NOTSET = 0, - DEBUG = 1, - INFO = 2, - WARNING = 3, - ERROR = 4, - } - export enum NOTIFICATION_TYPES { - ACTIVATE = 'ACTIVATE:experiment, user_id, attributes, variation, events', - TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event', - } - } - - declare module '@optimizely/optimizely-sdk/lib/plugins/event_dispatcher/index.node.js' { - - } - - declare module '@optimizely/optimizely-sdk/lib/utils/json_schema_validator' { - - } - - declare module '@optimizely/optimizely-sdk/lib/plugins/error_handler' { - } - - declare module '@optimizely/optimizely-sdk/lib/plugins/logger' { - import * as Optimizely from '@optimizely/optimizely-sdk'; - import * as enums from '@optimizely/optimizely-sdk/lib/utils/enums'; - - export interface Config { - logLevel?: enums.LOG_LEVEL, - logToConsole?: boolean, - prefix?: string, - } - export function createLogger(config: Config): Optimizely.Logger; - export function createNoOpLogger(): Optimizely.Logger; - } \ No newline at end of file diff --git a/packages/optimizely-sdk/lib/index.node.js b/packages/optimizely-sdk/lib/index.node.js deleted file mode 100644 index 743a11655..000000000 --- a/packages/optimizely-sdk/lib/index.node.js +++ /dev/null @@ -1,106 +0,0 @@ -/**************************************************************************** - * Copyright 2016-2017, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ -var logging = require('@optimizely/js-sdk-logging'); -var configValidator = require('./utils/config_validator'); -var defaultErrorHandler = require('./plugins/error_handler'); -var defaultEventDispatcher = require('./plugins/event_dispatcher/index.node'); -var enums = require('./utils/enums'); -var fns = require('./utils/fns'); -var jsonSchemaValidator = require('./utils/json_schema_validator'); -var loggerPlugin = require('./plugins/logger'); - -var Optimizely = require('./optimizely'); - -var logger = logging.getLogger(); -logging.setLogLevel(logging.LogLevel.ERROR); - -/** - * Entry point into the Optimizely Node testing SDK - */ -module.exports = { - logging: loggerPlugin, - errorHandler: defaultErrorHandler, - eventDispatcher: defaultEventDispatcher, - enums: enums, - - setLogger: logging.setLogHandler, - setLogLevel: logging.setLogLevel, - - /** - * Creates an instance of the Optimizely class - * @param {Object} config - * @param {Object} config.datafile - * @param {Object} config.errorHandler - * @param {Object} config.eventDispatcher - * @param {Object} config.jsonSchemaValidator - * @param {Object} config.logger - * @param {Object} config.userProfileService - * @return {Object} the Optimizely object - */ - createInstance: function(config) { - try { - var hasLogger = false; - config = config || {}; - - // TODO warn about setting per instance errorHandler / logger / logLevel - if (config.errorHandler) { - logging.setErrorHandler(config.errorHandler); - } - if (config.logger) { - // only set a logger in node if one is provided, by not setting we are noop-ing - hasLogger = true; - logging.setLogHandler(config.logger); - // respect the logger's shouldLog functionality - logging.setLogLevel(logging.LogLevel.NOTSET); - } - if (config.logLevel !== undefined) { - logging.setLogLevel(config.logLevel); - } - - try { - configValidator.validate(config); - config.isValidInstance = true; - } catch (ex) { - if (hasLogger) { - logger.error(ex); - } else { - console.error(ex.message); - } - config.isValidInstance = false; - } - - config = fns.assign( - { - eventDispatcher: defaultEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - skipJSONValidation: false, - }, - config, - { - clientEngine: enums.NODE_CLIENT_ENGINE, - // always get the OptimizelyLogger facade from logging - logger: logger, - errorHandler: logging.getErrorHandler(), - } - ); - - return new Optimizely(config); - } catch (e) { - logger.error(e); - return null; - } - }, -}; diff --git a/packages/optimizely-sdk/lib/index.node.tests.js b/packages/optimizely-sdk/lib/index.node.tests.js deleted file mode 100644 index 67aee38fd..000000000 --- a/packages/optimizely-sdk/lib/index.node.tests.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var logging = require('@optimizely/js-sdk-logging'); -var configValidator = require('./utils/config_validator'); -var enums = require('./utils/enums'); -var loggerPlugin = require('./plugins/logger'); -var Optimizely = require('./optimizely'); -var optimizelyFactory = require('./index.node'); - -var chai = require('chai'); -var assert = chai.assert; -var sinon = require('sinon'); - -describe('optimizelyFactory', function() { - describe('APIs', function() { - it('should expose logger, errorHandler, eventDispatcher and enums', function() { - assert.isDefined(optimizelyFactory.logging); - assert.isDefined(optimizelyFactory.logging.createLogger); - assert.isDefined(optimizelyFactory.logging.createNoOpLogger); - assert.isDefined(optimizelyFactory.errorHandler); - assert.isDefined(optimizelyFactory.eventDispatcher); - assert.isDefined(optimizelyFactory.enums); - }); - - describe('createInstance', function() { - var fakeErrorHandler = { handleError: function() {} }; - var fakeEventDispatcher = { dispatchEvent: function() {} }; - var fakeLogger; - - beforeEach(function() { - fakeLogger = { log: sinon.spy(), setLogLevel: sinon.spy() }; - sinon.stub(loggerPlugin, 'createLogger').returns(fakeLogger); - sinon.stub(configValidator, 'validate'); - sinon.stub(console, 'error'); - }); - - afterEach(function() { - loggerPlugin.createLogger.restore(); - configValidator.validate.restore(); - console.error.restore(); - }); - - it('should not throw if the provided config is not valid and log an error if logger is passed in', function() { - configValidator.validate.throws(new Error('Invalid config or something')); - var localLogger = loggerPlugin.createLogger({ logLevel: enums.LOG_LEVEL.INFO }); - assert.doesNotThrow(function() { - optimizelyFactory.createInstance({ - datafile: {}, - logger: localLogger, - }); - }); - sinon.assert.calledWith(localLogger.log, enums.LOG_LEVEL.ERROR); - }); - - it('should not throw if the provided config is not valid and log an error if no logger is provided', function() { - configValidator.validate.throws(new Error('Invalid config or something')); - assert.doesNotThrow(function() { - optimizelyFactory.createInstance({ - datafile: {}, - }); - }); - sinon.assert.calledOnce(console.error); - }); - - it('should create an instance of optimizely', function() { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: {}, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - }); - - assert.instanceOf(optlyInstance, Optimizely); - assert.equal(optlyInstance.clientVersion, '3.1.0-beta1'); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/optimizely/index.js b/packages/optimizely-sdk/lib/optimizely/index.js deleted file mode 100644 index a2090225d..000000000 --- a/packages/optimizely-sdk/lib/optimizely/index.js +++ /dev/null @@ -1,675 +0,0 @@ -/**************************************************************************** - * Copyright 2016-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var fns = require('../utils/fns'); -var attributesValidator = require('../utils/attributes_validator'); -var decisionService = require('../core/decision_service'); -var enums = require('../utils/enums'); -var eventBuilder = require('../core/event_builder/index.js'); -var eventTagsValidator = require('../utils/event_tags_validator'); -var notificationCenter = require('../core/notification_center'); -var projectConfig = require('../core/project_config'); -var projectConfigSchema = require('./project_config_schema'); -var sprintf = require('sprintf-js').sprintf; -var userProfileServiceValidator = require('../utils/user_profile_service_validator'); -var stringValidator = require('../utils/string_value_validator'); -var configValidator = require('../utils/config_validator'); - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MODULE_NAME = 'OPTIMIZELY'; -var DECISION_SOURCES = enums.DECISION_SOURCES; -var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; - -/** - * The Optimizely class - * @param {Object} config - * @param {string} config.clientEngine - * @param {string} config.clientVersion - * @param {Object} config.datafile - * @param {Object} config.errorHandler - * @param {Object} config.eventDispatcher - * @param {Object} config.logger - * @param {Object} config.skipJSONValidation - * @param {Object} config.userProfileService - */ -function Optimizely(config) { - var clientEngine = config.clientEngine; - if (clientEngine !== enums.NODE_CLIENT_ENGINE && clientEngine !== enums.JAVASCRIPT_CLIENT_ENGINE) { - config.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, MODULE_NAME, clientEngine)); - clientEngine = enums.NODE_CLIENT_ENGINE; - } - - this.clientEngine = clientEngine; - this.clientVersion = config.clientVersion || enums.NODE_CLIENT_VERSION; - this.errorHandler = config.errorHandler; - this.eventDispatcher = config.eventDispatcher; - this.isValidInstance = config.isValidInstance; - this.logger = config.logger; - - try { - configValidator.validateDatafile(config.datafile); - if (typeof config.datafile === 'string' || config.datafile instanceof String) { - config.datafile = JSON.parse(config.datafile); - } - - if (config.skipJSONValidation === true) { - this.configObj = projectConfig.createProjectConfig(config.datafile); - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.SKIPPING_JSON_VALIDATION, MODULE_NAME)); - } else { - if (config.jsonSchemaValidator.validate(projectConfigSchema, config.datafile)) { - this.configObj = projectConfig.createProjectConfig(config.datafile); - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.VALID_DATAFILE, MODULE_NAME)); - } - } - } catch (ex) { - this.isValidInstance = false; - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - } - - var userProfileService = null; - if (config.userProfileService) { - try { - if (userProfileServiceValidator.validate(config.userProfileService)) { - userProfileService = config.userProfileService; - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.VALID_USER_PROFILE_SERVICE, MODULE_NAME)); - } - } catch (ex) { - this.logger.log(LOG_LEVEL.WARNING, ex.message); - } - } - - this.decisionService = decisionService.createDecisionService({ - configObj: this.configObj, - userProfileService: userProfileService, - logger: this.logger, - }); - - this.notificationCenter = notificationCenter.createNotificationCenter({ - logger: this.logger, - errorHandler: this.errorHandler - }); -} - -/** - * Buckets visitor and sends impression event to Optimizely. - * @param {string} experimentKey - * @param {string} userId - * @param {Object} attributes - * @return {string|null} variation key - */ -Optimizely.prototype.activate = function (experimentKey, userId, attributes) { - try { - if (!this.isValidInstance) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'activate')); - return null; - } - - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId }, attributes)) { - return this.__notActivatingExperiment(experimentKey, userId); - } - - try { - var variationKey = this.getVariation(experimentKey, userId, attributes); - if (variationKey === null) { - return this.__notActivatingExperiment(experimentKey, userId); - } - - // If experiment is not set to 'Running' status, log accordingly and return variation key - if (!projectConfig.isRunning(this.configObj, experimentKey)) { - var shouldNotDispatchActivateLogMessage = sprintf(LOG_MESSAGES.SHOULD_NOT_DISPATCH_ACTIVATE, MODULE_NAME, experimentKey); - this.logger.log(LOG_LEVEL.DEBUG, shouldNotDispatchActivateLogMessage); - return variationKey; - } - - this._sendImpressionEvent(experimentKey, variationKey, userId, attributes); - - return variationKey; - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - var failedActivationLogMessage = sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, MODULE_NAME, userId, experimentKey); - this.logger.log(LOG_LEVEL.INFO, failedActivationLogMessage); - this.errorHandler.handleError(ex); - return null; - } - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Create an impression event and call the event dispatcher's dispatch method to - * send this event to Optimizely. Then use the notification center to trigger - * any notification listeners for the ACTIVATE notification type. - * @param {string} experimentKey Key of experiment that was activated - * @param {string} variationKey Key of variation shown in experiment that was activated - * @param {string} userId ID of user to whom the variation was shown - * @param {Object} attributes Optional user attributes - */ -Optimizely.prototype._sendImpressionEvent = function(experimentKey, variationKey, userId, attributes) { - var variationId = projectConfig.getVariationIdFromExperimentAndVariationKey(this.configObj, experimentKey, variationKey); - var experimentId = projectConfig.getExperimentId(this.configObj, experimentKey); - var impressionEventOptions = { - attributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: this.configObj, - experimentId: experimentId, - userId: userId, - variationId: variationId, - logger: this.logger, - }; - var impressionEvent = eventBuilder.getImpressionEvent(impressionEventOptions); - var dispatchedImpressionEventLogMessage = sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - MODULE_NAME, - impressionEvent.url, - JSON.stringify(impressionEvent.params)); - this.logger.log(LOG_LEVEL.DEBUG, dispatchedImpressionEventLogMessage); - var eventDispatcherCallback = function() { - var activatedLogMessage = sprintf(LOG_MESSAGES.ACTIVATE_USER, MODULE_NAME, userId, experimentKey); - this.logger.log(LOG_LEVEL.INFO, activatedLogMessage); - }.bind(this); - this.__dispatchEvent(impressionEvent, eventDispatcherCallback); - - var experiment = this.configObj.experimentKeyMap[experimentKey]; - var variation; - if (experiment && experiment.variationKeyMap) { - variation = experiment.variationKeyMap[variationKey]; - } - this.notificationCenter.sendNotifications( - enums.NOTIFICATION_TYPES.ACTIVATE, - { - experiment: experiment, - userId: userId, - attributes: attributes, - variation: variation, - logEvent: impressionEvent - } - ); -}; - -/** - * Sends conversion event to Optimizely. - * @param {string} eventKey - * @param {string} userId - * @param {string} attributes - * @param {Object} eventTags Values associated with the event. - */ -Optimizely.prototype.track = function(eventKey, userId, attributes, eventTags) { - try { - - if (!this.isValidInstance) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'track')); - return; - } - - try { - if (!this.__validateInputs({ user_id: userId, event_key: eventKey }, attributes, eventTags)) { - return; - } - - if (!projectConfig.eventWithKeyExists(this.configObj, eventKey)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EVENT_KEY, MODULE_NAME, eventKey)); - } - - // remove null values from eventTags - eventTags = this.__filterEmptyValues(eventTags); - var conversionEventOptions = { - attributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: this.configObj, - eventKey: eventKey, - eventTags: eventTags, - logger: this.logger, - userId: userId, - }; - var conversionEvent = eventBuilder.getConversionEvent(conversionEventOptions); - - var dispatchedConversionEventLogMessage = sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - MODULE_NAME, - conversionEvent.url, - JSON.stringify(conversionEvent.params)); - this.logger.log(LOG_LEVEL.DEBUG, dispatchedConversionEventLogMessage); - - var eventDispatcherCallback = function () { - var trackedLogMessage = sprintf(LOG_MESSAGES.TRACK_EVENT, MODULE_NAME, eventKey, userId); - this.logger.log(LOG_LEVEL.INFO, trackedLogMessage); - }.bind(this); - - this.__dispatchEvent(conversionEvent, eventDispatcherCallback); - - this.notificationCenter.sendNotifications( - enums.NOTIFICATION_TYPES.TRACK, - { - eventKey: eventKey, - userId: userId, - attributes: attributes, - eventTags: eventTags, - logEvent: conversionEvent - } - ); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - var failedTrackLogMessage = sprintf(LOG_MESSAGES.NOT_TRACKING_USER, MODULE_NAME, userId); - this.logger.log(LOG_LEVEL.INFO, failedTrackLogMessage); - this.errorHandler.handleError(ex); - } - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return; - } -}; - -/** - * Gets variation where visitor will be bucketed. - * @param {string} experimentKey - * @param {string} userId - * @param {Object} attributes - * @return {string|null} variation key - */ -Optimizely.prototype.getVariation = function(experimentKey, userId, attributes) { - try { - if (!this.isValidInstance) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getVariation')); - return null; - } - - try { - if (!this.__validateInputs({experiment_key: experimentKey, user_id: userId}, attributes)) { - return null; - } - - var experiment = this.configObj.experimentKeyMap[experimentKey]; - if (fns.isEmpty(experiment)) { - this.logger.log(LOG_LEVEL.DEBUG, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); - return null; - } - - return this.decisionService.getVariation(experimentKey, userId, attributes); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return null; - } - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** -* Force a user into a variation for a given experiment. -* @param {string} experimentKey -* @param {string} userId -* @param {string|null} variationKey user will be forced into. If null, then clear the existing experiment-to-variation mapping. -* @return boolean A boolean value that indicates if the set completed successfully. -*/ -Optimizely.prototype.setForcedVariation = function(experimentKey, userId, variationKey) { - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId })) { - return false; - } - - try { - return projectConfig.setForcedVariation(this.configObj, experimentKey, userId, variationKey, this.logger); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return false; - } -}; - -/** - * Gets the forced variation for a given user and experiment. - * @param {string} experimentKey - * @param {string} userId - * @return {string|null} The forced variation key. -*/ -Optimizely.prototype.getForcedVariation = function(experimentKey, userId) { - if (!this.__validateInputs({ experiment_key: experimentKey, user_id: userId })) { - return null; - } - - try { - return projectConfig.getForcedVariation(this.configObj, experimentKey, userId, this.logger); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return null; - } -}; - -/** - * Validate string inputs, user attributes and event tags. - * @param {string} stringInputs Map of string keys and associated values - * @param {Object} userAttributes Optional parameter for user's attributes - * @param {Object} eventTags Optional parameter for event tags - * @return {boolean} True if inputs are valid - * - */ -Optimizely.prototype.__validateInputs = function(stringInputs, userAttributes, eventTags) { - try { - // Null, undefined or non-string user Id is invalid. - if (stringInputs.hasOwnProperty('user_id')) { - var userId = stringInputs.user_id; - if (typeof userId !== 'string' || userId === null || userId === 'undefined') { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, MODULE_NAME, 'user_id')); - } - - delete stringInputs.user_id; - } - - var inputKeys = Object.keys(stringInputs); - for (var index = 0; index < inputKeys.length; index++) { - var key = inputKeys[index]; - if (!stringValidator.validate(stringInputs[key])) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, MODULE_NAME, key)); - } - } - if (userAttributes) { - attributesValidator.validate(userAttributes); - } - if (eventTags) { - eventTagsValidator.validate(eventTags); - } - return true; - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - return false; - } -}; - -/** - * Shows failed activation log message and returns null when user is not activated in experiment - * @param experimentKey - * @param userId - * @return {null} - */ -Optimizely.prototype.__notActivatingExperiment = function(experimentKey, userId) { - var failedActivationLogMessage = sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, MODULE_NAME, userId, experimentKey); - this.logger.log(LOG_LEVEL.INFO, failedActivationLogMessage); - return null; -}; - -/** - * Dispatches an event and executes the designated callback if the dispatch returns a promise - * @param eventToDispatch - * @param callback - */ -Optimizely.prototype.__dispatchEvent = function (eventToDispatch, callback) { - var eventDispatcherResponse = this.eventDispatcher.dispatchEvent(eventToDispatch, callback); - //checking that response value is a promise, not a request object - if (!fns.isEmpty(eventDispatcherResponse) && typeof eventDispatcherResponse.then === 'function') { - eventDispatcherResponse.then(function () { - callback(); - }); - } -}; - -/** - * Filters out attributes/eventTags with null or undefined values - * @param map - * @returns {Object} map - */ -Optimizely.prototype.__filterEmptyValues = function (map) { - for (var key in map) { - if (map.hasOwnProperty(key) && (map[key] === null || map[key] === undefined)) { - delete map[key]; - } - } - return map; -}; - -/** - * Returns true if the feature is enabled for the given user. - * @param {string} featureKey Key of feature which will be checked - * @param {string} userId ID of user which will be checked - * @param {Object} attributes Optional user attributes - * @return {boolean} True if the feature is enabled for the user, false otherwise - */ -Optimizely.prototype.isFeatureEnabled = function (featureKey, userId, attributes) { - try { - if (!this.isValidInstance) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'isFeatureEnabled')); - return false; - } - - if (!this.__validateInputs({ feature_key: featureKey, user_id: userId }, attributes)) { - return false; - } - - var feature = projectConfig.getFeatureFromKey(this.configObj, featureKey, this.logger); - if (!feature) { - return false; - } - - var decision = this.decisionService.getVariationForFeature(feature, userId, attributes); - var variation = decision.variation; - if (!!variation) { - if (decision.decisionSource === DECISION_SOURCES.EXPERIMENT) { - // got a variation from the exp, so we track the impression - this._sendImpressionEvent(decision.experiment.key, decision.variation.key, userId, attributes); - } - if (variation.featureEnabled === true) { - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.FEATURE_ENABLED_FOR_USER, MODULE_NAME, featureKey, userId)); - return true; - } - } - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.FEATURE_NOT_ENABLED_FOR_USER, MODULE_NAME, featureKey, userId)); - return false; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return false; - } -}; - -/** - * Returns an Array containing the keys of all features in the project that are - * enabled for the given user. - * @param {string} userId - * @param {Object} attributes - * @return {Array} Array of feature keys (strings) - */ -Optimizely.prototype.getEnabledFeatures = function (userId, attributes) { - try { - var enabledFeatures = []; - if (!this.isValidInstance) { - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'getEnabledFeatures')); - return enabledFeatures; - } - - if (!this.__validateInputs({ user_id: userId })) { - return enabledFeatures; - } - - fns.forOwn(this.configObj.featureKeyMap, function (feature) { - if (this.isFeatureEnabled(feature.key, userId, attributes)) { - enabledFeatures.push(feature.key); - } - }.bind(this)); - - return enabledFeatures; - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return []; - } -}; - -/** - * Helper method to get the value for a variable of a certain type attached to a - * feature flag. Returns null if the feature key is invalid, the variable key is - * invalid, the given variable type does not match the variable's actual type, - * or the variable value cannot be cast to the required type. - * - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} variableType Type of the variable whose value is being - * accessed (must be one of FEATURE_VARIABLE_TYPES - * in lib/utils/enums/index.js) - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {*} Value of the variable cast to the appropriate - * type, or null if the feature key is invalid, the - * variable key is invalid, or there is a mismatch - * with the type of the variable - */ -Optimizely.prototype._getFeatureVariableForType = function(featureKey, variableKey, variableType, userId, attributes) { - if (!this.isValidInstance) { - var apiName = 'getFeatureVariable' + variableType.charAt(0).toUpperCase() + variableType.slice(1); - this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, apiName)); - return null; - } - - if (!this.__validateInputs({feature_key: featureKey, variable_key: variableKey, user_id: userId}, attributes)) { - return null; - } - - var featureFlag = projectConfig.getFeatureFromKey(this.configObj, featureKey, this.logger); - if (!featureFlag) { - return null; - } - - var variable = projectConfig.getVariableForFeature(this.configObj, featureKey, variableKey, this.logger); - if (!variable) { - return null; - } - - if (variable.type !== variableType) { - this.logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.VARIABLE_REQUESTED_WITH_WRONG_TYPE, MODULE_NAME, variableType, variable.type)); - return null; - } - - var decision = this.decisionService.getVariationForFeature(featureFlag, userId, attributes); - var variableValue; - if (decision.variation !== null) { - variableValue = projectConfig.getVariableValueForVariation(this.configObj, variable, decision.variation, this.logger); - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.USER_RECEIVED_VARIABLE_VALUE, MODULE_NAME, variableKey, featureFlag.key, variableValue, userId)); - } else { - variableValue = variable.defaultValue; - this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.USER_RECEIVED_DEFAULT_VARIABLE_VALUE, MODULE_NAME, userId, variableKey, featureFlag.key)); - } - - return projectConfig.getTypeCastValue(variableValue, variableType, this.logger); -}; - -/** - * Returns value for the given boolean variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {boolean|null} Boolean value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableBoolean = function (featureKey, variableKey, userId, attributes) { - try { - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.BOOLEAN, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given double variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {number|null} Number value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableDouble = function (featureKey, variableKey, userId, attributes) { - try { - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.DOUBLE, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given integer variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {number|null} Number value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableInteger = function (featureKey, variableKey, userId, attributes) { - try { - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.INTEGER, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -/** - * Returns value for the given string variable attached to the given feature - * flag. - * @param {string} featureKey Key of the feature whose variable's value is - * being accessed - * @param {string} variableKey Key of the variable whose value is being - * accessed - * @param {string} userId ID for the user - * @param {Object} attributes Optional user attributes - * @return {string|null} String value of the variable, or null if the - * feature key is invalid, the variable key is - * invalid, or there is a mismatch with the type - * of the variable - */ -Optimizely.prototype.getFeatureVariableString = function (featureKey, variableKey, userId, attributes) { - try { - return this._getFeatureVariableForType(featureKey, variableKey, FEATURE_VARIABLE_TYPES.STRING, userId, attributes); - } catch (e) { - this.logger.log(LOG_LEVEL.ERROR, e.message); - this.errorHandler.handleError(e); - return null; - } -}; - -module.exports = Optimizely; diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js deleted file mode 100644 index 1585653db..000000000 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ /dev/null @@ -1,3652 +0,0 @@ -/**************************************************************************** - * Copyright 2016-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -var Optimizely = require('./'); -var audienceEvaluator = require('../core/audience_evaluator'); -var bluebird = require('bluebird'); -var bucketer = require('../core/bucketer'); -var enums = require('../utils/enums'); -var eventBuilder = require('../core/event_builder/index.js'); -var eventDispatcher = require('../plugins/event_dispatcher/index.node'); -var errorHandler = require('../plugins/error_handler'); -var fns = require('../utils/fns'); -var jsonSchemaValidator = require('../utils/json_schema_validator'); -var logger = require('../plugins/logger'); -var decisionService = require('../core/decision_service'); -var testData = require('../tests/test_data'); -var jsonSchemaValidator = require('../utils/json_schema_validator'); -var projectConfig = require('../core/project_config'); - -var chai = require('chai'); -var assert = chai.assert; -var sinon = require('sinon'); -var sprintf = require('sprintf-js').sprintf; -var uuid = require('uuid'); - -var ERROR_MESSAGES = enums.ERROR_MESSAGES; -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var DECISION_SOURCES = enums.DECISION_SOURCES; - -describe('lib/optimizely', function() { - describe('constructor', function() { - var stubErrorHandler = { handleError: function() {}}; - var stubEventDispatcher = { dispatchEvent: function() { return bluebird.resolve(null); } }; - var createdLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - beforeEach(function() { - sinon.stub(stubErrorHandler, 'handleError'); - sinon.stub(createdLogger, 'log'); - }); - - afterEach(function() { - stubErrorHandler.handleError.restore(); - createdLogger.log.restore(); - }); - - describe('constructor', function() { - it('should construct an instance of the Optimizely class', function() { - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - assert.instanceOf(optlyInstance, Optimizely); - sinon.assert.called(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); - }); - - it('should construct an instance of the Optimizely class when datafile is JSON string', function() { - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: JSON.stringify(testData.getTestProjectConfig()), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - assert.instanceOf(optlyInstance, Optimizely); - sinon.assert.called(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); - }); - - it('should log if the client engine passed in is invalid', function() { - new Optimizely({ - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - logger: createdLogger, - }); - - sinon.assert.called(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, 'OPTIMIZELY', 'undefined')); - }); - - it('should throw an error if a datafile is not passed into the constructor', function() { - var optly = new Optimizely({ - clientEngine: 'node-sdk', - errorHandler: stubErrorHandler, - logger: createdLogger, - }); - sinon.assert.calledOnce(stubErrorHandler.handleError); - var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'CONFIG_VALIDATOR')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, 'CONFIG_VALIDATOR')); - - assert.isFalse(optly.isValidInstance); - }); - - it('should throw an error if the datafile JSON is malformed', function() { - var invalidDatafileJSON = 'abc'; - - new Optimizely({ - clientEngine: 'node-sdk', - errorHandler: stubErrorHandler, - datafile: invalidDatafileJSON, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_MALFORMED, 'CONFIG_VALIDATOR')); - }); - - it('should throw an error if the datafile is not valid', function() { - var invalidDatafile = testData.getTestProjectConfig(); - delete invalidDatafile['projectId']; - - new Optimizely({ - clientEngine: 'node-sdk', - errorHandler: stubErrorHandler, - datafile: invalidDatafile, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - sinon.assert.calledOnce(stubErrorHandler.handleError); - var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', 'projectId', 'is missing and it is required')); - }); - - it('should log an error if the datafile version is not supported', function() { - new Optimizely({ - clientEngine: 'node-sdk', - errorHandler: stubErrorHandler, - datafile: testData.getUnsupportedVersionConfig(), - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - - sinon.assert.calledOnce(stubErrorHandler.handleError); - var errorMessage = stubErrorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_VERSION, 'CONFIG_VALIDATOR', '5')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_DATAFILE_VERSION, 'CONFIG_VALIDATOR', '5')); - }); - - describe('skipping JSON schema validation', function() { - beforeEach(function() { - sinon.spy(jsonSchemaValidator, 'validate'); - }); - - afterEach(function() { - jsonSchemaValidator.validate.restore(); - }); - - it('should skip JSON schema validation if skipJSONValidation is passed into instance args with `true` value', function() { - new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - logger: logger.createLogger({ logToConsole: false }), - skipJSONValidation: true, - }); - - sinon.assert.notCalled(jsonSchemaValidator.validate); - }); - - it('should not skip JSON schema validation if skipJSONValidation is passed into instance args with any value other than true', function() { - new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: stubErrorHandler, - eventDispatcher: stubEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - skipJSONValidation: 'hi', - }); - - sinon.assert.calledOnce(jsonSchemaValidator.validate); - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.VALID_DATAFILE, 'OPTIMIZELY')); - }); - }); - - describe('when a user profile service is provided', function() { - beforeEach(function() { - sinon.stub(decisionService, 'createDecisionService'); - }); - - afterEach(function() { - decisionService.createDecisionService.restore(); - }); - - it('should validate and pass the user profile service to the decision service', function() { - var userProfileServiceInstance = { - lookup: function() {}, - save: function() {}, - }; - - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - logger: createdLogger, - datafile: testData.getTestProjectConfig(), - jsonSchemaValidator: jsonSchemaValidator, - userProfileService: userProfileServiceInstance, - }); - - sinon.assert.calledWith(decisionService.createDecisionService, { - configObj: optlyInstance.configObj, - userProfileService: userProfileServiceInstance, - logger: createdLogger, - }); - - // Checking the second log message as the first one just says "Datafile is valid" - var logMessage = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage, 'OPTIMIZELY: Valid user profile service provided.'); - }); - - it('should pass in a null user profile to the decision service if the provided user profile is invalid', function() { - var invalidUserProfile = { - save: function() {}, - }; - - var optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - logger: createdLogger, - datafile: testData.getTestProjectConfig(), - jsonSchemaValidator: jsonSchemaValidator, - userProfileService: invalidUserProfile, - }); - - sinon.assert.calledWith(decisionService.createDecisionService, { - configObj: optlyInstance.configObj, - userProfileService: null, - logger: createdLogger, - }); - - // Checking the second log message as the first one just says "Datafile is valid" - var logMessage = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage, 'USER_PROFILE_SERVICE_VALIDATOR: Provided user profile service instance is in an invalid format: Missing function \'lookup\'.'); - }); - }); - }); - }); - - //tests separated out because promises don't work well with fake timers - describe('CustomEventDispatcher', function() { - var bucketStub; - var returnsPromiseEventDispatcher = { - dispatchEvent: function(eventObj) { - return bluebird.resolve(eventObj); - } - }; - - var createdLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - var eventDispatcherPromise; - beforeEach(function() { - bucketStub = sinon.stub(bucketer, 'bucket'); - eventDispatcherPromise = bluebird.resolve(); - sinon.stub(errorHandler, 'handleError'); - sinon.stub(createdLogger, 'log'); - sinon.stub(returnsPromiseEventDispatcher, 'dispatchEvent').returns(eventDispatcherPromise); - }); - - afterEach(function() { - bucketer.bucket.restore(); - errorHandler.handleError.restore(); - createdLogger.log.restore(); - returnsPromiseEventDispatcher.dispatchEvent.restore(); - }); - - it('should execute a custom dispatchEvent\'s promise in activate', function(done) { - var instance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: errorHandler, - eventDispatcher: returnsPromiseEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - bucketStub.returns('111129'); - - var activate = instance.activate('testExperiment', 'testUser'); - - assert.strictEqual(activate, 'variation'); - eventDispatcherPromise.then(function() { - var logMessage = createdLogger.log.args[5][1]; - //checking that we executed our callback after resolving the promise - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.ACTIVATE_USER, - 'OPTIMIZELY', - 'testUser', - 'testExperiment')); - done(); - }); - }); - - it('should execute a custom dispatchEvent\'s promise in track', function(done) { - var instance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - errorHandler: errorHandler, - eventDispatcher: returnsPromiseEventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - bucketStub.returns('111129'); - - var activate = instance.activate('testExperiment', 'testUser'); - - assert.strictEqual(activate, 'variation'); - instance.track('testEvent', 'testUser'); - //checking that we executed our callback after resolving the promise - eventDispatcherPromise.then(function() { - var logMessage = createdLogger.log.args[7][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.TRACK_EVENT, - 'OPTIMIZELY', - 'testEvent', - 'testUser')); - done(); - }); - }); - }); - - describe('APIs', function() { - var optlyInstance; - var bucketStub; - var clock; - - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - beforeEach(function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - - bucketStub = sinon.stub(bucketer, 'bucket'); - sinon.stub(eventDispatcher, 'dispatchEvent'); - sinon.stub(errorHandler, 'handleError'); - sinon.stub(createdLogger, 'log'); - sinon.stub(uuid, 'v4').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); - - clock = sinon.useFakeTimers(new Date().getTime()); - }); - - afterEach(function() { - bucketer.bucket.restore(); - eventDispatcher.dispatchEvent.restore(); - errorHandler.handleError.restore(); - createdLogger.log.restore(); - clock.restore(); - uuid.v4.restore(); - }); - - describe('#activate', function() { - it('should call bucketer and dispatchEvent with proper args and return variation key', function() { - bucketStub.returns('111129'); - var activate = optlyInstance.activate('testExperiment', 'testUser'); - assert.strictEqual(activate, 'variation'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '4', - 'experiment_id': '111127', - 'variation_id': '111129' - }], - 'events': [{ - 'entity_id': '4', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, - 'PROJECT_CONFIG', - 'testUser')); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should dispatch proper params for null value attributes', function() { - bucketStub.returns('122229'); - var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', {browser_type: 'firefox', 'test_null_attribute': null}); - assert.strictEqual(activate, 'variationWithAudience'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '5', - 'experiment_id': '122227', - 'variation_id': '122229' - }], - 'events': [{ - 'entity_id': '5', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, - 'PROJECT_CONFIG', - 'testUser')); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should call bucketer and dispatchEvent with proper args and return variation key if user is in audience', function() { - bucketStub.returns('122229'); - var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', {browser_type: 'firefox'}); - assert.strictEqual(activate, 'variationWithAudience'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '5', - 'experiment_id': '122227', - 'variation_id': '122229' - }], - 'events': [{ - 'entity_id': '5', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, - 'PROJECT_CONFIG', - 'testUser')); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - - }); - - it('should call activate and dispatchEvent with typed attributes and return variation key', function() { - bucketStub.returns('122229'); - var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', { - browser_type: 'firefox', - boolean_key: true, - integer_key: 10, - double_key: 3.14, - }); - assert.strictEqual(activate, 'variationWithAudience'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '5', - 'experiment_id': '122227', - 'variation_id': '122229' - }], - 'events': [{ - 'entity_id': '5', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }, { - 'entity_id': '323434545', - 'key': 'boolean_key', - 'type': 'custom', - 'value': true - }, { - 'entity_id': '616727838', - 'key': 'integer_key', - 'type': 'custom', - 'value': 10 - }, { - 'entity_id': '808797686', - 'key': 'double_key', - 'type': 'custom', - 'value': 3.14 - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, - 'PROJECT_CONFIG', - 'testUser')); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - describe('when experiment_bucket_map attribute is present', function() { - it('should call activate and respect attribute experiment_bucket_map', function() { - bucketStub.returns('111128'); // id of "control" variation - var activate = optlyInstance.activate('testExperiment', 'testUser', { - $opt_experiment_bucket_map: { - '111127': { - variation_id: '111129', // id of "variation" variation - }, - }, - }); - - assert.strictEqual(activate, 'variation'); - sinon.assert.notCalled(bucketer.bucket); - }); - }); - - it('should call bucketer and dispatchEvent with proper args and return variation key if user is in grouped experiment', function() { - bucketStub.returns('662'); - var activate = optlyInstance.activate('groupExperiment2', 'testUser'); - assert.strictEqual(activate, 'var2exp2'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '2', - 'experiment_id': '443', - 'variation_id': '662' - }], - 'events': [{ - 'entity_id': '2', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, - 'PROJECT_CONFIG', - 'testUser')); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should call bucketer and dispatchEvent with proper args and return variation key if user is in grouped experiment and is in audience', function() { - bucketStub.returns('552'); - var activate = optlyInstance.activate('groupExperiment1', 'testUser', {browser_type: 'firefox'}); - assert.strictEqual(activate, 'var2exp1'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '1', - 'experiment_id': '442', - 'variation_id': '552' - }], - 'events': [{ - 'entity_id': '1', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should not make a dispatch event call if variation ID is null', function() { - bucketStub.returns(null); - assert.isNull(optlyInstance.activate('testExperiment', 'testUser')); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.called(createdLogger.log); - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, - 'PROJECT_CONFIG', - 'testUser')); - - - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, - 'OPTIMIZELY', - 'testUser', - 'testExperiment')); - }); - - it('should return null if user is not in audience and user is not in group', function() { - assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'testUser', {browser_type: 'chrome'})); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.DEBUG, - sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'testUser') - ); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, 'DECISION_SERVICE', 'testUser', 'testExperimentWithAudiences') - ); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.INFO , - sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentWithAudiences') - ); - }); - - it('should return null if user is not in audience and user is in group', function() { - assert.isNull(optlyInstance.activate('groupExperiment1', 'testUser', {browser_type: 'chrome'})); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.DEBUG, - sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'testUser') - ); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, 'DECISION_SERVICE', 'testUser', 'groupExperiment1') - ); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.INFO , - sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'groupExperiment1') - ); - }); - - it('should return null if experiment is not running', function() { - assert.isNull(optlyInstance.activate('testExperimentNotRunning', 'testUser')); - sinon.assert.calledTwice(createdLogger.log); - - var logMessage1 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage1, sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning')); - var logMessage2 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage2, sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentNotRunning')); - }); - - it('should throw an error for invalid user ID', function() { - assert.isNull(optlyInstance.activate('testExperiment', null)); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - - sinon.assert.calledTwice(createdLogger.log); - - var logMessage1 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage1, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - var logMessage2 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage2, sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'null', 'testExperiment')); - }); - - it('should log an error for invalid experiment key', function() { - assert.isNull(optlyInstance.activate('invalidExperimentKey', 'testUser')); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - - sinon.assert.calledTwice(createdLogger.log); - var logMessage1 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage1, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'OPTIMIZELY', 'invalidExperimentKey')); - var logMessage2 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage2, sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'invalidExperimentKey')); - }); - - it('should throw an error for invalid attributes', function() { - assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'testUser', [])); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - - sinon.assert.calledTwice(createdLogger.log); - var logMessage1 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage1, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - var logMessage2 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage2, sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentWithAudiences')); - }); - - it('should activate when logger is in DEBUG mode', function() { - bucketStub.returns('111129'); - var instance = new Optimizely({ - datafile: testData.getTestProjectConfig(), - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: logger.createLogger({ - logLevel: enums.LOG_LEVEL.DEBUG, - logToConsole: false, - }), - isValidInstance: true, - }); - - var variation = instance.activate('testExperiment', 'testUser'); - assert.strictEqual(variation, 'variation'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - }); - - describe('whitelisting', function() { - beforeEach(function() { - sinon.spy(Optimizely.prototype, '__validateInputs'); - }); - - afterEach(function() { - Optimizely.prototype.__validateInputs.restore(); - }); - - it('should return forced variation after experiment status check and before audience check', function() { - var activate = optlyInstance.activate('testExperiment', 'user1'); - assert.strictEqual(activate, 'control'); - - sinon.assert.calledTwice(Optimizely.prototype.__validateInputs); - sinon.assert.calledThrice(createdLogger.log); - - var logMessage0 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage0, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'user1')); - var logMessage1 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage1, sprintf(LOG_MESSAGES.USER_FORCED_IN_VARIATION, 'DECISION_SERVICE', 'user1', 'control')); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'decisions': [{ - 'campaign_id': '4', - 'experiment_id': '111127', - 'variation_id': '111128' - }], - 'events': [{ - 'entity_id': '4', - 'timestamp': Math.round(new Date().getTime()), - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - }] - }], - 'visitor_id': 'user1', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - - var logMessage2 = createdLogger.log.args[2][1]; - assert.strictEqual(logMessage2, sprintf(LOG_MESSAGES.DISPATCH_IMPRESSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - }); - - it('returns the variation key but does not dispatch the event if user is in experiment and experiment is set to Launched', function() { - bucketStub.returns('144448'); - - var bucketedVariation = optlyInstance.activate('testExperimentLaunched', 'testUser'); - assert.strictEqual(bucketedVariation, 'controlLaunched'); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - - it('should not activate when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.activate('testExperiment', 'testUser'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'activate')); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - }); - - describe('#track', function() { - it('should dispatch an event when no attributes are provided and the event\'s experiment is untargeted', function() { - optlyInstance.track('testEvent', 'testUser'); - - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - sinon.assert.called(createdLogger.log); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should dispatch an event when empty attributes are provided and the event\'s experiment is untargeted', function() { - optlyInstance.track('testEvent', 'testUser', {}); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should dispatch an event when attributes are provided and the event\'s experiment is untargeted', function() { - optlyInstance.track('testEvent', 'testUser', { browser_type: 'safari' }); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [ - { - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'safari', - }, - ], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should dispatch an event when no attributes are provided and the event\'s experiment is targeted', function() { - optlyInstance.track('testEventWithAudiences', 'testUser'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111097', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithAudiences' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should dispatch an event when empty attributes are provided and the event\'s experiment is targeted', function() { - optlyInstance.track('testEventWithAudiences', 'testUser'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111097', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithAudiences' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should call dispatchEvent with proper args when including null value attributes', function() { - optlyInstance.track('testEventWithAudiences', 'testUser', {browser_type: 'firefox', 'test_null_attribute': null}); - - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - sinon.assert.calledOnce(createdLogger.log); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111097', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithAudiences' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should call dispatchEvent with proper args when including attributes', function() { - optlyInstance.track('testEventWithAudiences', 'testUser', {browser_type: 'firefox'}); - - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - sinon.assert.calledOnce(createdLogger.log); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111097', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithAudiences' - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should call bucketer and dispatchEvent with proper args when including event tags', function() { - optlyInstance.track('testEvent', 'testUser', undefined, {eventTag: 'chill'}); - - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - sinon.assert.calledOnce(createdLogger.log); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent', - 'tags': { - 'eventTag': 'chill' - } - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should call dispatchEvent with proper args when including event tags and revenue', function() { - optlyInstance.track('testEvent', 'testUser', undefined, {revenue: 4200, eventTag: 'chill'}); - - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - sinon.assert.calledTwice(createdLogger.log); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent', - 'revenue': 4200, - 'tags': { - 'revenue': 4200, - 'eventTag': 'chill' - } - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - var logMessage0 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage0, sprintf(LOG_MESSAGES.PARSED_REVENUE_VALUE, - 'EVENT_TAG_UTILS', - '4200')); - var logMessage1 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage1, sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should call dispatchEvent with proper args when including event tags and null event tag values and revenue', function() { - optlyInstance.track('testEvent', 'testUser', undefined, {revenue: 4200, eventTag: 'chill', 'testNullEventTag': null}); - - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - sinon.assert.calledTwice(createdLogger.log); - - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEvent', - 'revenue': 4200, - 'tags': { - 'revenue': 4200, - 'eventTag': 'chill' - } - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - - var logMessage0 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage0, sprintf(LOG_MESSAGES.PARSED_REVENUE_VALUE, - 'EVENT_TAG_UTILS', - '4200')); - var logMessage1 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage1, sprintf(LOG_MESSAGES.DISPATCH_CONVERSION_EVENT, - 'OPTIMIZELY', - expectedObj.url, - JSON.stringify(expectedObj.params))); - }); - - it('should not call dispatchEvent when including invalid event value', function() { - optlyInstance.track('testEvent', 'testUser', undefined, '4200'); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledOnce(createdLogger.log); - }); - - it('should track a user for an experiment not running', function() { - optlyInstance.track('testEventWithExperimentNotRunning', 'testUser'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111099', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithExperimentNotRunning', - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should track a user when user is not in the audience of the experiment', function() { - optlyInstance.track('testEventWithAudiences', 'testUser', {browser_type: 'chrome'}); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111097', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithAudiences', - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'chrome' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should track a user when the event has no associated experiments', function() { - optlyInstance.track('testEventWithoutExperiments', 'testUser'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111098', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithoutExperiments', - }] - }], - 'visitor_id': 'testUser', - 'attributes': [], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should only send one conversion event when the event is attached to multiple experiments', function() { - optlyInstance.track('testEventWithMultipleExperiments', 'testUser', {browser_type: 'firefox'}); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedObj = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'snapshots': [{ - 'events': [{ - 'entity_id': '111100', - 'timestamp': Math.round(new Date().getTime()), - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'testEventWithMultipleExperiments', - }] - }], - 'visitor_id': 'testUser', - 'attributes': [{ - 'entity_id': '111094', - 'key': 'browser_type', - 'type': 'custom', - 'value': 'firefox' - }], - }], - 'revision': '42', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': false, - 'enrich_decisions': true, - }, - }; - var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0]; - assert.deepEqual(eventDispatcherCall[0], expectedObj); - }); - - it('should throw an error for invalid user ID', function() { - optlyInstance.track('testEvent', null); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - - it('should throw an error for invalid event key', function() { - optlyInstance.track('invalidEventKey', 'testUser'); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_EVENT_KEY, 'OPTIMIZELY', 'invalidEventKey')); - - sinon.assert.calledTwice(createdLogger.log); - var logMessage1 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage1, sprintf(ERROR_MESSAGES.INVALID_EVENT_KEY, 'OPTIMIZELY', 'invalidEventKey')); - - var logMessage2 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage2, sprintf(LOG_MESSAGES.NOT_TRACKING_USER, 'OPTIMIZELY', 'testUser')); - }); - - it('should throw an error for invalid attributes', function() { - optlyInstance.track('testEvent', 'testUser', []); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - }); - - it('should not throw an error for an event key without associated experiment IDs', function() { - optlyInstance.track('testEventWithoutExperiments', 'testUser'); - sinon.assert.notCalled(errorHandler.handleError); - }); - - it('should track when logger is in DEBUG mode', function() { - var instance = new Optimizely({ - datafile: testData.getTestProjectConfig(), - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: logger.createLogger({ - logLevel: enums.LOG_LEVEL.DEBUG, - logToConsole: false, - }), - isValidInstance: true, - }); - - instance.track('testEvent', 'testUser'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - }); - - it('should not track when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.track('testExperiment', 'testUser'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'track')); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - }); - - describe('#getVariation', function() { - it('should call bucketer and return variation key', function() { - bucketStub.returns('111129'); - var getVariation = optlyInstance.getVariation('testExperiment', 'testUser'); - - assert.strictEqual(getVariation, 'variation'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.called(createdLogger.log); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.DEBUG, - sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'testUser') - ); - }); - - it('should call bucketer and return variation key with attributes', function() { - bucketStub.returns('122229'); - var getVariation = optlyInstance.getVariation('testExperimentWithAudiences', - 'testUser', - {browser_type: 'firefox'}); - - assert.strictEqual(getVariation, 'variationWithAudience'); - - sinon.assert.calledOnce(bucketer.bucket); - sinon.assert.called(createdLogger.log); - }); - - it('should return null if user is not in audience or experiment is not running', function() { - var getVariationReturnsNull1 = optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', {}); - var getVariationReturnsNull2 = optlyInstance.getVariation('testExperimentNotRunning', 'testUser'); - - assert.isNull(getVariationReturnsNull1); - assert.isNull(getVariationReturnsNull2); - - sinon.assert.notCalled(bucketer.bucket); - sinon.assert.called(createdLogger.log); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.DEBUG, - sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'testUser') - ); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, 'DECISION_SERVICE', 'testUser', 'testExperimentWithAudiences') - ); - - sinon.assert.calledWithExactly( - createdLogger.log, - LOG_LEVEL.INFO, - sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning') - ); - }); - - it('should throw an error for invalid user ID', function() { - var getVariationWithError = optlyInstance.getVariation('testExperiment', null); - - assert.isNull(getVariationWithError); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - - it('should log an error for invalid experiment key', function() { - var getVariationWithError = optlyInstance.getVariation('invalidExperimentKey', 'testUser'); - assert.isNull(getVariationWithError); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'OPTIMIZELY', 'invalidExperimentKey')); - }); - - it('should throw an error for invalid attributes', function() { - var getVariationWithError = optlyInstance.getVariation('testExperimentWithAudiences', 'testUser', []); - - assert.isNull(getVariationWithError); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - }); - - describe('whitelisting', function() { - beforeEach(function() { - sinon.spy(Optimizely.prototype, '__validateInputs'); - }); - - afterEach(function() { - Optimizely.prototype.__validateInputs.restore(); - }); - - it('should return forced variation after experiment status check and before audience check', function() { - var getVariation = optlyInstance.getVariation('testExperiment', 'user1'); - assert.strictEqual(getVariation, 'control'); - - sinon.assert.calledOnce(Optimizely.prototype.__validateInputs); - - sinon.assert.calledTwice(createdLogger.log); - - var logMessage0 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage0, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'user1')); - var logMessage = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.USER_FORCED_IN_VARIATION, 'DECISION_SERVICE', 'user1', 'control')); - }); - }); - - it('should not return variation when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.getVariation('testExperiment', 'testUser'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'getVariation')); - - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - - describe('order of bucketing operations', function() { - it('should properly follow the order of bucketing operations', function() { - // Order of operations is preconditions > experiment is running > whitelisting > audience eval > variation bucketing - bucketStub.returns('122228'); // returns the control variation - - // invalid user, running experiment - assert.isNull(optlyInstance.activate('testExperiment', 123)); - - // valid user, experiment not running, whitelisted - assert.isNull(optlyInstance.activate('testExperimentNotRunning', 'user1')); - - // valid user, experiment running, not whitelisted, does not meet audience conditions - assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'user3')); - - // valid user, experiment running, not whitelisted, meets audience conditions - assert.strictEqual(optlyInstance.activate('testExperimentWithAudiences', 'user3', { browser_type: 'firefox' }), 'controlWithAudience'); - - // valid user, running experiment, whitelisted, does not meet audience conditions - // expect user to be forced into `variationWithAudience` through whitelisting - assert.strictEqual(optlyInstance.activate('testExperimentWithAudiences', 'user2', { browser_type: 'chrome' }), 'variationWithAudience'); - - // valid user, running experiment, whitelisted, meets audience conditions - // expect user to be forced into `variationWithAudience (122229)` through whitelisting - assert.strictEqual(optlyInstance.activate('testExperimentWithAudiences', 'user2', { browser_type: 'firefox' }), 'variationWithAudience'); - }); - }); - }); - - describe('#getForcedVariation', function() { - it('should return null when set has not been called', function() { - var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); - assert.strictEqual(forcedVariation, null); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'PROJECT_CONFIG', 'user1')); - }); - - it('should return null with a null experimentKey', function() { - var forcedVariation = optlyInstance.getForcedVariation(null, 'user1'); - assert.strictEqual(forcedVariation, null); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); - }); - - it('should return null with an undefined experimentKey', function() { - var forcedVariation = optlyInstance.getForcedVariation(undefined, 'user1'); - assert.strictEqual(forcedVariation, null); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); - }); - - it('should return null with a null userId', function() { - var forcedVariation = optlyInstance.getForcedVariation('testExperiment', null); - assert.strictEqual(forcedVariation, null); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - - it('should return null with an undefined userId', function() { - var forcedVariation = optlyInstance.getForcedVariation('testExperiment', undefined); - assert.strictEqual(forcedVariation, null); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - }); - - describe('#setForcedVariation', function() { - it('should be able to set a forced variation', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); - assert.strictEqual(didSetVariation, true); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.USER_MAPPED_TO_FORCED_VARIATION, 'PROJECT_CONFIG', 111128, 111127, 'user1')); - }); - - it('should override bucketing in optlyInstance.getVariation', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); - assert.strictEqual(didSetVariation, true); - - var variation = optlyInstance.getVariation('testExperiment', 'user1', {}); - assert.strictEqual(variation, 'control'); - }); - - it('should be able to set and get forced variation', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); - assert.strictEqual(didSetVariation, true); - - var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); - assert.strictEqual(forcedVariation, 'control'); - }); - - it('should be able to set, unset, and get forced variation', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); - assert.strictEqual(didSetVariation, true); - - var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); - assert.strictEqual(forcedVariation, 'control'); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperiment', 'user1', null); - assert.strictEqual(didSetVariation2, true); - - var forcedVariation2 = optlyInstance.getForcedVariation('testExperiment', 'user1'); - assert.strictEqual(forcedVariation2, null); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - var variationIsMappedLogMessage = createdLogger.log.args[1][1]; - var variationMappingRemovedLogMessage = createdLogger.log.args[2][1]; - - assert.strictEqual(setVariationLogMessage, sprintf(LOG_MESSAGES.USER_MAPPED_TO_FORCED_VARIATION, 'PROJECT_CONFIG', 111128, 111127, 'user1')); - - assert.strictEqual(variationIsMappedLogMessage, sprintf(LOG_MESSAGES.USER_HAS_FORCED_VARIATION, 'PROJECT_CONFIG', 'control', 'testExperiment', 'user1')); - - assert.strictEqual(variationMappingRemovedLogMessage, sprintf(LOG_MESSAGES.VARIATION_REMOVED_FOR_USER, 'PROJECT_CONFIG', 'testExperiment', 'user1')); - }); - - it('should be able to set multiple experiments for one user', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); - assert.strictEqual(didSetVariation, true); - - var didSetVariation2 = optlyInstance.setForcedVariation('testExperimentLaunched', 'user1', 'variationLaunched'); - assert.strictEqual(didSetVariation2, true); - - var forcedVariation = optlyInstance.getForcedVariation('testExperiment', 'user1'); - assert.strictEqual(forcedVariation, 'control'); - - var forcedVariation2 = optlyInstance.getForcedVariation('testExperimentLaunched', 'user1'); - assert.strictEqual(forcedVariation2, 'variationLaunched'); - }); - - it('should not set an invalid variation', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'definitely_not_valid_variation_key'); - assert.strictEqual(didSetVariation, false); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.NO_VARIATION_FOR_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'definitely_not_valid_variation_key', 'testExperiment')); - }); - - it('should not set an invalid experiment', function() { - var didSetVariation = optlyInstance.setForcedVariation('definitely_not_valid_exp_key', 'user1', 'control'); - assert.strictEqual(didSetVariation, false); - - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.EXPERIMENT_KEY_NOT_IN_DATAFILE, 'PROJECT_CONFIG', 'definitely_not_valid_exp_key')); - }); - - it('should return null for user has no forced variation for experiment', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', 'control'); - assert.strictEqual(didSetVariation, true); - - var forcedVariation = optlyInstance.getForcedVariation('testExperimentLaunched', 'user1'); - assert.strictEqual(forcedVariation, null); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(LOG_MESSAGES.USER_MAPPED_TO_FORCED_VARIATION, 'PROJECT_CONFIG', 111128, 111127, 'user1')); - - var noVariationToGetLogMessage = createdLogger.log.args[1][1]; - assert.strictEqual(noVariationToGetLogMessage, sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT, 'PROJECT_CONFIG', 'testExperimentLaunched', 'user1')); - }); - - it('should return false for a null experimentKey', function() { - var didSetVariation = optlyInstance.setForcedVariation(null, 'user1', 'control'); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); - }); - - it('should return false for an undefined experimentKey', function() { - var didSetVariation = optlyInstance.setForcedVariation(undefined, 'user1', 'control'); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); - }); - - it('should return false for an empty experimentKey', function() { - var didSetVariation = optlyInstance.setForcedVariation('', 'user1', 'control'); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'experiment_key')); - }); - - it('should return false for a null userId', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', null, 'control'); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - - it('should return false for an undefined userId', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', undefined, 'control'); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - - it('should return true for an empty userId', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', '', 'control'); - assert.strictEqual(didSetVariation, true); - }); - - it('should return false for a null variationKey', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', null); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.USER_NOT_IN_FORCED_VARIATION, 'PROJECT_CONFIG', 'user1')); - }); - - it('should return false for an undefined variationKey', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperiment', 'user1', undefined); - assert.strictEqual(didSetVariation, false); - - var setVariationLogMessage = createdLogger.log.args[0][1]; - assert.strictEqual(setVariationLogMessage, sprintf(ERROR_MESSAGES.USER_NOT_IN_FORCED_VARIATION, 'PROJECT_CONFIG', 'user1')); - }); - - it('should not override check for not running experiments in getVariation', function() { - var didSetVariation = optlyInstance.setForcedVariation('testExperimentNotRunning', 'user1', 'controlNotRunning'); - assert.strictEqual(didSetVariation, true); - - var variation = optlyInstance.getVariation('testExperimentNotRunning', 'user1', {}); - assert.strictEqual(variation, null); - - var logMessage0 = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage0, sprintf(LOG_MESSAGES.USER_MAPPED_TO_FORCED_VARIATION, 'PROJECT_CONFIG', 133338, 133337, 'user1')); - - var logMessage1 = createdLogger.log.args[1][1]; - assert.strictEqual(logMessage1, sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning')); - }); - }); - - describe('__validateInputs', function() { - it('should return true if user ID and attributes are valid', function() { - assert.isTrue(optlyInstance.__validateInputs({user_id: 'testUser'})); - assert.isTrue(optlyInstance.__validateInputs({user_id: ''})); - assert.isTrue(optlyInstance.__validateInputs({user_id: 'testUser'}, {browser_type: 'firefox'})); - sinon.assert.notCalled(createdLogger.log); - }); - - it('should return false and throw an error if user ID is invalid', function() { - var falseUserIdInput = optlyInstance.__validateInputs({user_id: []}); - assert.isFalse(falseUserIdInput); - - falseUserIdInput = optlyInstance.__validateInputs({user_id: null}); - assert.isFalse(falseUserIdInput); - - falseUserIdInput = optlyInstance.__validateInputs({user_id: 3.14}); - assert.isFalse(falseUserIdInput); - - sinon.assert.calledThrice(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - - sinon.assert.calledThrice(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id')); - }); - - it('should return false and throw an error if attributes are invalid', function() { - var falseUserIdInput = optlyInstance.__validateInputs({user_id: 'testUser'}, []); - assert.isFalse(falseUserIdInput); - - sinon.assert.calledOnce(errorHandler.handleError); - var errorMessage = errorHandler.handleError.lastCall.args[0].message; - assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR')); - }); - }); - - describe('should filter out null values', function() { - it('should filter out a null value', function() { - var dict = {'test': null}; - var filteredValue = optlyInstance.__filterEmptyValues(dict); - assert.deepEqual(filteredValue, {}); - }); - - it('should filter out a undefined value', function() { - var dict = {'test': undefined}; - var filteredValue = optlyInstance.__filterEmptyValues(dict); - assert.deepEqual(filteredValue, {}); - }); - - it('should filter out a null value, leave a non null one', function() { - var dict = {'test': null, 'test2': 'not_null'}; - var filteredValue = optlyInstance.__filterEmptyValues(dict); - assert.deepEqual(filteredValue, {'test2': 'not_null'}); - }); - - it('should not filter out a non empty value', function() { - var dict = {'test': 'hello'}; - var filteredValue = optlyInstance.__filterEmptyValues(dict); - assert.deepEqual(filteredValue, {'test': 'hello'}); - }); - }); - - describe('notification listeners', function() { - var decisionListener; - var trackListener; - var decisionListener2; - var trackListener2; - - beforeEach(function() { - decisionListener = sinon.spy(); - trackListener = sinon.spy(); - decisionListener2 = sinon.spy(); - trackListener2 = sinon.spy(); - bucketStub.returns('111129'); - sinon.stub(fns, 'currentTimestamp').returns(1509489766569); - }); - - afterEach(function() { - fns.currentTimestamp.restore(); - }); - - it('should call a listener added for activate when activate is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - var variationKey = optlyInstance.activate('testExperiment', 'testUser'); - assert.strictEqual(variationKey, 'variation'); - sinon.assert.calledOnce(decisionListener); - }); - - it('should call a listener added for track when track is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - sinon.assert.calledOnce(trackListener); - }); - - it('should not call a removed activate listener when activate is called', function() { - var listenerId = optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.notificationCenter.removeNotificationListener(listenerId); - var variationKey = optlyInstance.activate('testExperiment', 'testUser'); - assert.strictEqual(variationKey, 'variation'); - sinon.assert.notCalled(decisionListener); - }); - - it('should not call a removed track listener when track is called', function() { - var listenerId = optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.notificationCenter.removeNotificationListener(listenerId); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - sinon.assert.notCalled(trackListener); - }); - - it('removeNotificationListener should only remove the listener with the argument ID', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - var trackListenerId = optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.notificationCenter.removeNotificationListener(trackListenerId); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - sinon.assert.calledOnce(decisionListener); - }); - - it('should clear all notification listeners when clearAllNotificationListeners is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.notificationCenter.clearAllNotificationListeners(); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - - sinon.assert.notCalled(decisionListener); - sinon.assert.notCalled(trackListener); - }); - - it('should clear listeners of certain notification type when clearNotificationListeners is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.notificationCenter.clearNotificationListeners(enums.NOTIFICATION_TYPES.ACTIVATE); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - - sinon.assert.notCalled(decisionListener); - sinon.assert.calledOnce(trackListener); - }); - - it('should only call the listener once after the same listener was added twice', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.activate('testExperiment', 'testUser'); - sinon.assert.calledOnce(decisionListener); - }); - - it('should not add a listener with an invalid type argument', function() { - var listenerId = optlyInstance.notificationCenter.addNotificationListener( - 'not a notification type', - decisionListener - ); - assert.strictEqual(listenerId, -1); - optlyInstance.activate('testExperiment', 'testUser'); - sinon.assert.notCalled(decisionListener); - optlyInstance.track('testEvent', 'testUser'); - sinon.assert.notCalled(decisionListener); - }); - - it('should call multiple notification listeners for activate when activate is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener2 - ); - optlyInstance.activate('testExperiment', 'testUser'); - sinon.assert.calledOnce(decisionListener); - sinon.assert.calledOnce(decisionListener2); - }); - - it('should call multiple notification listeners for track when track is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener2 - ); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - sinon.assert.calledOnce(trackListener); - sinon.assert.calledOnce(trackListener2); - }); - - it('should pass the correct arguments to an activate listener when activate is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.activate('testExperiment', 'testUser'); - var expectedImpressionEvent = { - httpVerb: 'POST', - url: 'https://logx.optimizely.com/v1/events', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - snapshots: [ - { - decisions: [ - { - campaign_id: '4', - experiment_id: '111127', - variation_id: '111129', - }, - ], - events: [ - { - entity_id: '4', - timestamp: 1509489766569, - key: 'campaign_activated', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - }, - ], - }, - ], - visitor_id: 'testUser', - attributes: [], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: enums.NODE_CLIENT_VERSION, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - var instanceExperiments = optlyInstance.configObj.experiments; - var expectedArgument = { - experiment: instanceExperiments[0], - userId: 'testUser', - attributes: undefined, - variation: instanceExperiments[0].variations[1], - logEvent: expectedImpressionEvent, - }; - sinon.assert.calledWith(decisionListener, expectedArgument); - }); - - it('should pass the correct arguments to an activate listener when activate is called with attributes', function() { - var attributes = { - browser_type: 'firefox', - }; - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.ACTIVATE, - decisionListener - ); - optlyInstance.activate('testExperiment', 'testUser', attributes); - var expectedImpressionEvent = { - httpVerb: 'POST', - url: 'https://logx.optimizely.com/v1/events', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - snapshots: [ - { - decisions: [ - { - campaign_id: '4', - experiment_id: '111127', - variation_id: '111129', - }, - ], - events: [ - { - entity_id: '4', - timestamp: 1509489766569, - key: 'campaign_activated', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - }, - ], - }, - ], - visitor_id: 'testUser', - attributes: [ - { - entity_id: '111094', - key: 'browser_type', - type: 'custom', - value: 'firefox', - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: enums.NODE_CLIENT_VERSION, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - var instanceExperiments = optlyInstance.configObj.experiments; - var expectedArgument = { - experiment: instanceExperiments[0], - userId: 'testUser', - attributes: attributes, - variation: instanceExperiments[0].variations[1], - logEvent: expectedImpressionEvent, - }; - sinon.assert.calledWith(decisionListener, expectedArgument); - }); - - it('should pass the correct arguments to a track listener when track is called', function() { - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.activate('testExperiment', 'testUser'); - optlyInstance.track('testEvent', 'testUser'); - var expectedConversionEvent = { - httpVerb: 'POST', - url: 'https://logx.optimizely.com/v1/events', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - snapshots: [ - { - events: [ - { - entity_id: '111095', - timestamp: 1509489766569, - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEvent', - }, - ], - }, - ], - visitor_id: 'testUser', - attributes: [], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: enums.NODE_CLIENT_VERSION, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - var expectedArgument = { - eventKey: 'testEvent', - userId: 'testUser', - attributes: undefined, - eventTags: undefined, - logEvent: expectedConversionEvent, - }; - sinon.assert.calledWith(trackListener, expectedArgument); - }); - - it('should pass the correct arguments to a track listener when track is called with attributes', function() { - var attributes = { - browser_type: 'firefox', - }; - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.activate('testExperiment', 'testUser', attributes); - optlyInstance.track('testEvent', 'testUser', attributes); - var expectedConversionEvent = { - httpVerb: 'POST', - url: 'https://logx.optimizely.com/v1/events', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - snapshots: [ - { - events: [ - { - entity_id: '111095', - timestamp: 1509489766569, - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEvent', - }, - ], - }, - ], - visitor_id: 'testUser', - attributes: [ - { - entity_id: '111094', - key: 'browser_type', - type: 'custom', - value: 'firefox', - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: enums.NODE_CLIENT_VERSION, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - var expectedArgument = { - eventKey: 'testEvent', - userId: 'testUser', - attributes: attributes, - eventTags: undefined, - logEvent: expectedConversionEvent, - }; - sinon.assert.calledWith(trackListener, expectedArgument); - }); - - it('should pass the correct arguments to a track listener when track is called with attributes and event tags', function() { - var attributes = { - browser_type: 'firefox', - }; - var eventTags = { - value: 1.234, - non_revenue: 'abc', - }; - optlyInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.TRACK, - trackListener - ); - optlyInstance.activate('testExperiment', 'testUser', attributes); - optlyInstance.track('testEvent', 'testUser', attributes, eventTags); - var expectedConversionEvent = { - httpVerb: 'POST', - url: 'https://logx.optimizely.com/v1/events', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - snapshots: [ - { - events: [ - { - entity_id: '111095', - timestamp: 1509489766569, - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEvent', - tags: { - non_revenue: 'abc', - value: 1.234, - }, - value: 1.234, - }, - ], - }, - ], - visitor_id: 'testUser', - attributes: [ - { - entity_id: '111094', - key: 'browser_type', - type: 'custom', - value: 'firefox', - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: enums.NODE_CLIENT_VERSION, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - var expectedArgument = { - eventKey: 'testEvent', - userId: 'testUser', - attributes: attributes, - eventTags: eventTags, - logEvent: expectedConversionEvent, - }; - sinon.assert.calledWith(trackListener, expectedArgument); - }); - }); - }); - - //tests separated out from APIs because of mock bucketing - describe('getVariationBucketingIdAttribute', function() { - var optlyInstance; - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - beforeEach(function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfig(), - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - }); - - var userAttributes = { - 'browser_type': 'firefox', - }; - var userAttributesWithBucketingId = { - 'browser_type': 'firefox', - '$opt_bucketing_id': '123456789' - }; - - it('confirm that a valid variation is bucketed without the bucketing ID', function() { - assert.strictEqual('controlWithAudience', optlyInstance.getVariation( - 'testExperimentWithAudiences', - 'testUser', - userAttributes - )); - }); - - it('confirm that an invalid audience returns null', function() { - assert.strictEqual(null, optlyInstance.getVariation( - 'testExperimentWithAudiences', - 'testUser' - )); - }); - - it('confirm that a valid variation is bucketed with the bucketing ID', function() { - assert.strictEqual('variationWithAudience', optlyInstance.getVariation( - 'testExperimentWithAudiences', - 'testUser', - userAttributesWithBucketingId - )); - }); - - it('confirm that invalid experiment with the bucketing ID returns null', function() { - assert.strictEqual(null, optlyInstance.getVariation( - 'invalidExperimentKey', - 'testUser', - userAttributesWithBucketingId - )); - }); - }); - - describe('feature management', function() { - var sandbox = sinon.sandbox.create(); - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - var optlyInstance; - var clock; - beforeEach(function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestProjectConfigWithFeatures(), - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - - sandbox.stub(eventDispatcher, 'dispatchEvent'); - sandbox.stub(errorHandler, 'handleError'); - sandbox.stub(createdLogger, 'log'); - sandbox.stub(uuid, 'v4').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); - sandbox.stub(fns, 'currentTimestamp').returns(1509489766569); - clock = sinon.useFakeTimers(new Date().getTime()); - }); - - afterEach(function() { - sandbox.restore(); - clock.restore(); - }); - - describe('#isFeatureEnabled', function() { - it('returns false, and does not dispatch an impression event, for an invalid feature key', function() { - var result = optlyInstance.isFeatureEnabled('thisIsDefinitelyNotAFeatureKey', 'user1'); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - - it('returns false if the instance is invalid', function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: { - lasers: 300, - message: 'this is not a valid datafile' - }, - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user1'); - assert.strictEqual(result, false); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Optimizely object is not valid. Failing isFeatureEnabled.'); - }); - - describe('when the user bucketed into a variation of an experiment with the feature', function() { - var attributes = { test_attribute: 'test_value' }; - - describe('when the variation is toggled ON', function() { - beforeEach(function() { - var experiment = optlyInstance.configObj.experimentKeyMap.testing_my_feature; - var variation = experiment.variations[0]; - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }); - }); - - it('returns true and dispatches an impression event', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 'user1', attributes); - assert.strictEqual(result, true); - sinon.assert.calledOnce(optlyInstance.decisionService.getVariationForFeature); - var feature = optlyInstance.configObj.featureKeyMap.test_feature_for_experiment; - sinon.assert.calledWithExactly( - optlyInstance.decisionService.getVariationForFeature, - feature, - 'user1', - attributes - ); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedImpressionEvent = { - 'httpVerb': 'POST', - 'url': 'https://logx.optimizely.com/v1/events', - 'params': { - 'account_id': '572018', - 'project_id': '594001', - 'visitors': [ - { - 'snapshots': [ - { - 'decisions': [ - { - 'campaign_id': '594093', - 'experiment_id': '594098', - 'variation_id': '594096' - } - ], - 'events': [ - { - 'entity_id': '594093', - 'timestamp': 1509489766569, - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - } - ] - } - ], - 'visitor_id': 'user1', - 'attributes': [ - { - 'entity_id': '594014', - 'key': 'test_attribute', - 'type': 'custom', - 'value': 'test_value', - }, { - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering', - 'type': 'custom', - 'value': true, - }, - ], - } - ], - 'revision': '35', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': true, - 'enrich_decisions': true, - }, - }; - var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; - assert.deepEqual(callArgs[0], expectedImpressionEvent); - assert.isFunction(callArgs[1]); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Feature test_feature_for_experiment is enabled for user user1.'); - }); - - it('returns false and does not dispatch an impression event when feature key is null', function() { - var result = optlyInstance.isFeatureEnabled(null, 'user1', attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided feature_key is in an invalid format.'); - }); - - it('returns false when user id is null', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', null, attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns false when feature key and user id are null', function() { - var result = optlyInstance.isFeatureEnabled(null, null, attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns false when feature key is undefined', function() { - var result = optlyInstance.isFeatureEnabled(undefined, 'user1', attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided feature_key is in an invalid format.'); - }); - - it('returns false when user id is undefined', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', undefined, attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns false when feature key and user id are undefined', function() { - var result = optlyInstance.isFeatureEnabled(undefined, undefined, attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - - it('returns false when no arguments are provided', function() { - var result = optlyInstance.isFeatureEnabled(); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns false when user id is an object', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', {}, attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns false when user id is a number', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', 72, attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns false when feature key is an array', function() { - var result = optlyInstance.isFeatureEnabled(['a', 'feature'], 'user1', attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided feature_key is in an invalid format.'); - }); - - it('returns true when user id is an empty string', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment', '', attributes); - assert.strictEqual(result, true); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - }); - - it('returns false when feature key is an empty string', function() { - var result = optlyInstance.isFeatureEnabled('', 'user1', attributes); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - - it('returns false when a feature key is provided, but a user id is not', function() { - var result = optlyInstance.isFeatureEnabled('test_feature_for_experiment'); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - }); - - describe('when the variation is toggled OFF', function() { - var result; - beforeEach(function() { - var experiment = optlyInstance.configObj.experimentKeyMap.test_shared_feature; - var variation = experiment.variations[1]; - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }); - result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); - }); - - it('should return false', function() { - assert.strictEqual(result, false); - sinon.assert.calledOnce(optlyInstance.decisionService.getVariationForFeature); - var feature = optlyInstance.configObj.featureKeyMap.shared_feature; - sinon.assert.calledWithExactly( - optlyInstance.decisionService.getVariationForFeature, - feature, - 'user1', - attributes - ); - }); - - it('should dispatch an impression event', function() { - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - var expectedImpressionEvent = { - 'httpVerb': 'POST', - 'url': 'https://logx.optimizely.com/v1/events', - 'params': { - 'account_id': '572018', - 'project_id': '594001', - 'visitors': [ - { - 'snapshots': [ - { - 'decisions': [ - { - 'campaign_id': '599023', - 'experiment_id': '599028', - 'variation_id': '599027' - } - ], - 'events': [ - { - 'entity_id': '599023', - 'timestamp': 1509489766569, - 'key': 'campaign_activated', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - } - ] - } - ], - 'visitor_id': 'user1', - 'attributes': [ - { - 'entity_id': '594014', - 'key': 'test_attribute', - 'type': 'custom', - 'value': 'test_value', - }, { - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering', - 'type': 'custom', - 'value': true, - }, - ], - } - ], - 'revision': '35', - 'client_name': 'node-sdk', - 'client_version': enums.NODE_CLIENT_VERSION, - 'anonymize_ip': true, - 'enrich_decisions': true, - }, - }; - var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; - assert.deepEqual(callArgs[0], expectedImpressionEvent); - assert.isFunction(callArgs[1]); - }); - }); - - describe('when the variation is missing the toggle', function() { - beforeEach(function() { - var experiment = optlyInstance.configObj.experimentKeyMap.test_shared_feature; - var variation = fns.cloneDeep(experiment.variations[0]); - delete variation['featureEnabled']; - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }); - }); - - it('should return false', function() { - var result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); - assert.strictEqual(result, false); - sinon.assert.calledOnce(optlyInstance.decisionService.getVariationForFeature); - var feature = optlyInstance.configObj.featureKeyMap.shared_feature; - sinon.assert.calledWithExactly( - optlyInstance.decisionService.getVariationForFeature, - feature, - 'user1', - attributes - ); - }); - }); - }); - - describe('user bucketed into a variation of a rollout of the feature', function() { - describe('when the variation is toggled ON', function() { - beforeEach(function() { - // This experiment is the first audience targeting rule in the rollout of feature 'test_feature' - var experiment = optlyInstance.configObj.experimentKeyMap['594031']; - var variation = experiment.variations[0]; - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.ROLLOUT, - }); - }); - - it('returns true and does not dispatch an event', function() { - var result = optlyInstance.isFeatureEnabled('test_feature', 'user1', { - test_attribute: 'test_value', - }); - assert.strictEqual(result, true); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Feature test_feature is enabled for user user1.'); - }); - }); - - describe('when the variation is toggled OFF', function() { - beforeEach(function() { - // This experiment is the second audience targeting rule in the rollout of feature 'test_feature' - var experiment = optlyInstance.configObj.experimentKeyMap['594037']; - var variation = experiment.variations[0]; - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.ROLLOUT, - }); - }); - - it('returns false ', function() { - var result = optlyInstance.isFeatureEnabled('test_feature', 'user1', { - test_attribute: 'test_value', - }); - assert.strictEqual(result, false); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Feature test_feature is not enabled for user user1.'); - }); - }); - }); - - describe('user not bucketed into an experiment or a rollout', function() { - beforeEach(function() { - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: null, - variation: null, - decisionSource: null, - }); - }); - - it('returns false and does not dispatch an event', function() { - var result = optlyInstance.isFeatureEnabled('test_feature', 'user1'); - assert.strictEqual(result, false); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Feature test_feature is not enabled for user user1.'); - }); - }); - }); - - describe('#getEnabledFeatures', function() { - beforeEach(function() { - sandbox.stub(optlyInstance, 'isFeatureEnabled').callsFake(function(featureKey) { - return featureKey === 'test_feature' || featureKey === 'test_feature_for_experiment'; - }); - }); - - it('returns an empty array if the instance is invalid', function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: { - lasers: 300, - message: 'this is not a valid datafile' - }, - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - }); - var result = optlyInstance.getEnabledFeatures('user1', { test_attribute: 'test_value' }); - assert.deepEqual(result, []); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Optimizely object is not valid. Failing getEnabledFeatures.'); - }); - - it('returns only enabled features for the specified user and attributes', function() { - var attributes = { test_attribute: 'test_value', }; - var result = optlyInstance.getEnabledFeatures('user1', attributes); - assert.strictEqual(result.length, 2); - assert.isAbove(result.indexOf('test_feature'), -1); - assert.isAbove(result.indexOf('test_feature_for_experiment'), -1); - sinon.assert.callCount(optlyInstance.isFeatureEnabled, 7); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'test_feature', - 'user1', - attributes - ); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'test_feature_2', - 'user1', - attributes - ); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'test_feature_for_experiment', - 'user1', - attributes - ); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'feature_with_group', - 'user1', - attributes - ); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'shared_feature', - 'user1', - attributes - ); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'unused_flag', - 'user1', - attributes - ); - sinon.assert.calledWithExactly( - optlyInstance.isFeatureEnabled, - 'feature_exp_no_traffic', - 'user1', - attributes - ); - }); - }); - - describe('feature variable APIs', function() { - describe('bucketed into variation in an experiment with variable values', function() { - beforeEach(function() { - var experiment = optlyInstance.configObj.experimentKeyMap.testing_my_feature; - var variation = experiment.variations[0]; - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: experiment, - variation: variation, - decisionSource: DECISION_SOURCES.EXPERIMENT, - }); - }); - - it('returns the right value from getFeatureVariableBoolean', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, true); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Value for variable "is_button_animated" of feature flag "test_feature_for_experiment" is true for user "user1"'); - }); - - it('returns the right value from getFeatureVariableDouble', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, 20.25); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Value for variable "button_width" of feature flag "test_feature_for_experiment" is 20.25 for user "user1"'); - }); - - it('returns the right value from getFeatureVariableInteger', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, 2); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Value for variable "num_buttons" of feature flag "test_feature_for_experiment" is 2 for user "user1"'); - }); - - it('returns the right value from getFeatureVariableString', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, 'Buy me NOW'); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: Value for variable "button_txt" of feature flag "test_feature_for_experiment" is Buy me NOW for user "user1"'); - }); - - it('returns null from getFeatureVariableBoolean when called with a non-boolean variable', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'button_width', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.WARNING, 'OPTIMIZELY: Requested variable type "boolean", but variable is of type "double". Use correct API to retrieve value. Returning None.'); - }); - - it('returns null from getFeatureVariableDouble when called with a non-double variable', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'is_button_animated', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.WARNING, 'OPTIMIZELY: Requested variable type "double", but variable is of type "boolean". Use correct API to retrieve value. Returning None.'); - }); - - it('returns null from getFeatureVariableInteger when called with a non-integer variable', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'button_width', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.WARNING, 'OPTIMIZELY: Requested variable type "integer", but variable is of type "double". Use correct API to retrieve value. Returning None.'); - }); - - it('returns null from getFeatureVariableString when called with a non-string variable', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'num_buttons', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.WARNING, 'OPTIMIZELY: Requested variable type "string", but variable is of type "integer". Use correct API to retrieve value. Returning None.'); - }); - - it('returns null from getFeatureVariableBoolean if user id is null', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated', null, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableBoolean if user id is undefined', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated', undefined, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableBoolean if user id is not provided', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableDouble if user id is null', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', null, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableDouble if user id is undefined', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', undefined, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableDouble if user id is not provided', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableInteger if user id is null', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', null, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableInteger if user id is undefined', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', undefined, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableInteger if user id is not provided', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableString if user id is null', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', null, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableString if user id is undefined', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', undefined, { test_attribute: 'test_value' }); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - it('returns null from getFeatureVariableString if user id is not provided', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'OPTIMIZELY: Provided user_id is in an invalid format.'); - }); - - describe('type casting failures', function() { - describe('invalid boolean', function() { - beforeEach(function() { - sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('falsezzz'); - }); - - it('should return null and log an error', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Unable to cast value falsezzz to type boolean, returning null.'); - }); - }); - - describe('invalid integer', function() { - beforeEach(function() { - sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('zzz123'); - }); - - it('should return null and log an error', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Unable to cast value zzz123 to type integer, returning null.'); - }); - }); - - describe('invalid double', function() { - beforeEach(function() { - sandbox.stub(projectConfig, 'getVariableValueForVariation').returns('zzz44.55'); - }); - - it('should return null and log an error', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Unable to cast value zzz44.55 to type double, returning null.'); - }); - }); - }); - }); - - describe('not bucketed into a variation', function() { - beforeEach(function() { - sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns({ - experiment: null, - variation: null, - decisionSource: null, - }); - }); - - it('returns the variable default value from getFeatureVariableBoolean', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'is_button_animated', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, false); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "is_button_animated" of feature flag "test_feature_for_experiment".'); - }); - - it('returns the variable default value from getFeatureVariableDouble', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'button_width', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, 50.55); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_width" of feature flag "test_feature_for_experiment".'); - }); - - it('returns the variable default value from getFeatureVariableInteger', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'num_buttons', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, 10); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "num_buttons" of feature flag "test_feature_for_experiment".'); - }); - - it('returns the variable default value from getFeatureVariableString', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'button_txt', 'user1', { test_attribute: 'test_value' }); - assert.strictEqual(result, 'Buy me'); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.INFO, 'OPTIMIZELY: User "user1" is not in any variation or rollout rule. Returning default value for variable "button_txt" of feature flag "test_feature_for_experiment".'); - }); - }); - - it('returns null from getFeatureVariableBoolean if the argument feature key is invalid', function() { - var result = optlyInstance.getFeatureVariableBoolean('thisIsNotAValidKey<><><>', 'is_button_animated', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Feature key thisIsNotAValidKey<><><> is not in datafile.'); - }); - - it('returns null from getFeatureVariableDouble if the argument feature key is invalid', function() { - var result = optlyInstance.getFeatureVariableDouble('thisIsNotAValidKey<><><>', 'button_width', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Feature key thisIsNotAValidKey<><><> is not in datafile.'); - }); - - it('returns null from getFeatureVariableInteger if the argument feature key is invalid', function() { - var result = optlyInstance.getFeatureVariableInteger('thisIsNotAValidKey<><><>', 'num_buttons', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Feature key thisIsNotAValidKey<><><> is not in datafile.'); - }); - - it('returns null from getFeatureVariableString if the argument feature key is invalid', function() { - var result = optlyInstance.getFeatureVariableString('thisIsNotAValidKey<><><>', 'button_txt', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Feature key thisIsNotAValidKey<><><> is not in datafile.'); - }); - - it('returns null from getFeatureVariableBoolean if the argument variable key is invalid', function() { - var result = optlyInstance.getFeatureVariableBoolean('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Variable with key "thisIsNotAVariableKey****" associated with feature with key "test_feature_for_experiment" is not in datafile.'); - }); - - it('returns null from getFeatureVariableDouble if the argument variable key is invalid', function() { - var result = optlyInstance.getFeatureVariableDouble('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Variable with key "thisIsNotAVariableKey****" associated with feature with key "test_feature_for_experiment" is not in datafile.'); - }); - - it('returns null from getFeatureVariableInteger if the argument variable key is invalid', function() { - var result = optlyInstance.getFeatureVariableInteger('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Variable with key "thisIsNotAVariableKey****" associated with feature with key "test_feature_for_experiment" is not in datafile.'); - }); - - it('returns null from getFeatureVariableString if the argument variable key is invalid', function() { - var result = optlyInstance.getFeatureVariableString('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - assert.strictEqual(result, null); - sinon.assert.calledWith(createdLogger.log, LOG_LEVEL.ERROR, 'PROJECT_CONFIG: Variable with key "thisIsNotAVariableKey****" associated with feature with key "test_feature_for_experiment" is not in datafile.'); - }); - - it('returns null from getFeatureVariableBoolean when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.getFeatureVariableBoolean('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'getFeatureVariableBoolean')); - }); - - it('returns null from getFeatureVariableDouble when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.getFeatureVariableDouble('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'getFeatureVariableDouble')); - }); - - it('returns null from getFeatureVariableInteger when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.getFeatureVariableInteger('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'getFeatureVariableInteger')); - }); - - it('returns null from getFeatureVariableString when optimizely object is not a valid instance', function() { - var instance = new Optimizely({ - datafile: {}, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - logger: createdLogger, - }); - - createdLogger.log.reset(); - - instance.getFeatureVariableString('test_feature_for_experiment', 'thisIsNotAVariableKey****', 'user1'); - - sinon.assert.calledOnce(createdLogger.log); - var logMessage = createdLogger.log.args[0][1]; - assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'getFeatureVariableString')); - }); - }); - }); - - describe('audience match types', function() { - var sandbox = sinon.sandbox.create(); - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - var optlyInstance; - beforeEach(function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTypedAudiencesConfig(), - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - - sandbox.stub(eventDispatcher, 'dispatchEvent'); - sandbox.stub(errorHandler, 'handleError'); - sandbox.stub(createdLogger, 'log'); - }); - - afterEach(function() { - sandbox.restore(); - }); - - it('can activate an experiment with a typed audience', function() { - var variationKey = optlyInstance.activate('typed_audience_experiment', 'user1', { - // Should be included via exact match string audience with id '3468206642' - house: 'Gryffindor', - }); - assert.strictEqual(variationKey, 'A'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - assert.includeDeepMembers( - eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, - [{ entity_id: '594015', key: 'house', type: 'custom', value: 'Gryffindor' }] - ); - - variationKey = optlyInstance.activate('typed_audience_experiment', 'user1', { - // Should be included via exact match number audience with id '3468206646' - lasers: 45.5, - }); - assert.strictEqual(variationKey, 'A'); - sinon.assert.calledTwice(eventDispatcher.dispatchEvent); - assert.includeDeepMembers( - eventDispatcher.dispatchEvent.getCall(1).args[0].params.visitors[0].attributes, - [{ entity_id: '594016', key: 'lasers', type: 'custom', value: 45.5 }] - ); - }); - - it('can exclude a user from an experiment with a typed audience via activate', function() { - var variationKey = optlyInstance.activate('typed_audience_experiment', 'user1', { - house: 'Hufflepuff', - }); - assert.isNull(variationKey); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - }); - - it('can track an experiment with a typed audience', function() { - optlyInstance.track('item_bought', 'user1', { - // Should be included via substring match string audience with id '3988293898' - house: 'Welcome to Slytherin!', - }); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - assert.includeDeepMembers( - eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, - [{ entity_id: '594015', key: 'house', type: 'custom', value: 'Welcome to Slytherin!' }] - ); - }); - - it('can include a user in a rollout with a typed audience via isFeatureEnabled', function() { - var featureEnabled = optlyInstance.isFeatureEnabled('feat', 'user1', { - // Should be included via exists match audience with id '3988293899' - favorite_ice_cream: 'chocolate', - }); - assert.isTrue(featureEnabled); - - featureEnabled = optlyInstance.isFeatureEnabled('feat', 'user1', { - // Should be included via less-than match audience with id '3468206644' - lasers: -3, - }); - assert.isTrue(featureEnabled); - }); - - it('can exclude a user from a rollout with a typed audience via isFeatureEnabled', function() { - var featureEnabled = optlyInstance.isFeatureEnabled('feat', 'user1', {}); - assert.isFalse(featureEnabled); - }); - - it('can return a variable value from a feature test with a typed audience via getFeatureVariableString', function() { - var variableValue = optlyInstance.getFeatureVariableString('feat_with_var', 'x', 'user1', { - // Should be included in the feature test via greater-than match audience with id '3468206647' - lasers: 71, - }); - assert.strictEqual(variableValue, 'xyz'); - - variableValue = optlyInstance.getFeatureVariableString('feat_with_var', 'x', 'user1', { - // Should be included in the feature test via exact match boolean audience with id '3468206643' - should_do_it: true, - }); - assert.strictEqual(variableValue, 'xyz'); - }); - - it('can return the default value from a feature variable from getFeatureVariableString, via excluding a user from a feature test with a typed audience', function() { - var variableValue = optlyInstance.getFeatureVariableString('feat_with_var', 'x', 'user1', { - lasers: 50, - }); - assert.strictEqual(variableValue, 'x'); - }); - }); - - describe('audience combinations', function() { - var sandbox = sinon.sandbox.create(); - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - var optlyInstance; - beforeEach(function() { - optlyInstance = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTypedAudiencesConfig(), - eventBuilder: eventBuilder, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - }); - - sandbox.stub(eventDispatcher, 'dispatchEvent'); - sandbox.stub(errorHandler, 'handleError'); - sandbox.stub(createdLogger, 'log'); - sandbox.spy(audienceEvaluator, 'evaluate'); - }); - - afterEach(function() { - sandbox.restore(); - }); - - it('can activate an experiment with complex audience conditions', function() { - var variationKey = optlyInstance.activate('audience_combinations_experiment', 'user1', { - // Should be included via substring match string audience with id '3988293898', and - // exact match number audience with id '3468206646' - house: 'Welcome to Slytherin!', - lasers: 45.5, - }); - assert.strictEqual(variationKey, 'A'); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - assert.includeDeepMembers( - eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, - [ - { entity_id: '594015', key: 'house', type: 'custom', value: 'Welcome to Slytherin!' }, - { entity_id: '594016', key: 'lasers', type: 'custom', value: 45.5 }, - ] - ); - sinon.assert.calledWithExactly( - audienceEvaluator.evaluate, - optlyInstance.configObj.experiments[2].audienceConditions, - optlyInstance.configObj.audiencesById, - { house: 'Welcome to Slytherin!', lasers: 45.5 }, - createdLogger - ); - }); - - it('can exclude a user from an experiment with complex audience conditions', function() { - var variationKey = optlyInstance.activate('audience_combinations_experiment', 'user1', { - // Should be excluded - substring string audience with id '3988293898' does not match, - // so the overall conditions fail - house: 'Hufflepuff', - lasers: 45.5, - }); - assert.isNull(variationKey); - sinon.assert.notCalled(eventDispatcher.dispatchEvent); - sinon.assert.calledWithExactly( - audienceEvaluator.evaluate, - optlyInstance.configObj.experiments[2].audienceConditions, - optlyInstance.configObj.audiencesById, - { house: 'Hufflepuff', lasers: 45.5 }, - createdLogger - ); - }); - - it('can track an experiment with complex audience conditions', function() { - optlyInstance.track('user_signed_up', 'user1', { - // Should be included via exact match string audience with id '3468206642', and - // exact match boolean audience with id '3468206643' - house: 'Gryffindor', - should_do_it: true, - }); - sinon.assert.calledOnce(eventDispatcher.dispatchEvent); - assert.includeDeepMembers( - eventDispatcher.dispatchEvent.getCall(0).args[0].params.visitors[0].attributes, - [ - { entity_id: '594015', key: 'house', type: 'custom', value: 'Gryffindor' }, - { entity_id: '594017', key: 'should_do_it', type: 'custom', value: true } - ] - ); - }); - - it('can include a user in a rollout with complex audience conditions via isFeatureEnabled', function() { - var featureEnabled = optlyInstance.isFeatureEnabled('feat2', 'user1', { - // Should be included via substring match string audience with id '3988293898', and - // exists audience with id '3988293899' - house: '...Slytherinnn...sss.', - favorite_ice_cream: 'matcha', - }); - assert.isTrue(featureEnabled); - sinon.assert.calledWithExactly( - audienceEvaluator.evaluate, - optlyInstance.configObj.rollouts[2].experiments[0].audienceConditions, - optlyInstance.configObj.audiencesById, - { house: '...Slytherinnn...sss.', favorite_ice_cream: 'matcha' }, - createdLogger - ); - }); - - it('can exclude a user from a rollout with complex audience conditions via isFeatureEnabled', function() { - var featureEnabled = optlyInstance.isFeatureEnabled('feat2', 'user1', { - // Should be excluded - substring match string audience with id '3988293898' does not match, - // and no audience in the other branch of the 'and' matches either - house: 'Lannister', - }); - assert.isFalse(featureEnabled); - sinon.assert.calledWithExactly( - audienceEvaluator.evaluate, - optlyInstance.configObj.rollouts[2].experiments[0].audienceConditions, - optlyInstance.configObj.audiencesById, - { house: 'Lannister' }, - createdLogger - ); - }); - - it('can return a variable value from a feature test with complex audience conditions via getFeatureVariableString', function() { - var variableValue = optlyInstance.getFeatureVariableInteger('feat2_with_var', 'z', 'user1', { - // Should be included via exact match string audience with id '3468206642', and - // greater than audience with id '3468206647' - house: 'Gryffindor', - lasers: 700, - }); - assert.strictEqual(variableValue, 150); - sinon.assert.calledWithExactly( - audienceEvaluator.evaluate, - optlyInstance.configObj.experiments[3].audienceConditions, - optlyInstance.configObj.audiencesById, - { house: 'Gryffindor', lasers: 700 }, - createdLogger - ); - }); - - it('can return the default value for a feature variable from getFeatureVariableString, via excluding a user from a feature test with complex audience conditions', function() { - var variableValue = optlyInstance.getFeatureVariableInteger('feat2_with_var', 'z', 'user1', { - // Should be excluded - no audiences match with no attributes - }); - assert.strictEqual(variableValue, 10); - sinon.assert.calledWithExactly( - audienceEvaluator.evaluate, - optlyInstance.configObj.experiments[3].audienceConditions, - optlyInstance.configObj.audiencesById, - {}, - createdLogger - ); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/optimizely/project_config_schema.js b/packages/optimizely-sdk/lib/optimizely/project_config_schema.js deleted file mode 100644 index a2e96ef6a..000000000 --- a/packages/optimizely-sdk/lib/optimizely/project_config_schema.js +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/*eslint-disable */ -/** - * Project Config JSON Schema file used to validate the project json datafile - */ -module.exports = { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "projectId": { - "type": "string", - "required": true - }, - "accountId": { - "type": "string", - "required": true - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "policy": { - "type": "string", - "required": true - }, - "trafficAllocation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "entityId": { - "type": "string", - "required": true - }, - "endOfRange": { - "type": "integer", - "required": true - } - } - }, - "required": true - }, - "experiments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "key": { - "type": "string", - "required": true - }, - "status": { - "type": "string", - "required": true - }, - "layerId": { - "type": "string", - "required": true - }, - "variations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "key": { - "type": "string", - "required": true - } - } - }, - "required": true - }, - "trafficAllocation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "entityId": { - "type": "string", - "required": true - }, - "endOfRange": { - "type": "integer", - "required": true - } - } - }, - "required": true - }, - "audienceIds": { - "type": "array", - "items": { - "type": "string" - }, - "required": true - }, - "forcedVariations": { - "type": "object", - "required": true - } - } - }, - "required": true - } - } - }, - "required": true - }, - "experiments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "key": { - "type": "string", - "required": true - }, - "status": { - "type": "string", - "required": true - }, - "layerId": { - "type": "string", - "required": true - }, - "variations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "key": { - "type": "string", - "required": true - } - } - }, - "required": true - }, - "trafficAllocation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "entityId": { - "type": "string", - "required": true - }, - "endOfRange": { - "type": "integer", - "required": true - } - } - }, - "required": true - }, - "audienceIds": { - "type": "array", - "items": { - "type": "string" - }, - "required": true - }, - "forcedVariations": { - "type": "object", - "required": true - } - } - }, - "required": true - }, - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "required": true - }, - "experimentIds": { - "type": "array", - "items": { - "type": "string", - "required": true - } - }, - "id": { - "type": "string", - "required": true - } - } - }, - "required": true - }, - "audiences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "name": { - "type": "string", - "required": true - }, - "conditions": { - "type": "string", - "required": true - } - } - }, - "required": true - }, - "attributes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "required": true - }, - "key": { - "type": "string", - "required": true - }, - } - }, - "required": true - }, - "version": { - "type": "string", - "required": true - }, - "revision": { - "type": "string", - "required": true - }, - } -}; diff --git a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.browser.js b/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.browser.js deleted file mode 100644 index ffebd0a95..000000000 --- a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.browser.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var fns = require('../../utils/fns'); - -var POST_METHOD = 'POST'; -var GET_METHOD = 'GET'; -var READYSTATE_COMPLETE = 4; - -module.exports = { - /** - * Sample event dispatcher implementation for tracking impression and conversions - * Users of the SDK can provide their own implementation - * @param {Object} eventObj - * @param {Function} callback - */ - dispatchEvent: function(eventObj, callback) { - var url = eventObj.url; - var params = eventObj.params; - if (eventObj.httpVerb === POST_METHOD) { - var req = new XMLHttpRequest(); - req.open(POST_METHOD, url, true); - req.setRequestHeader('Content-Type', 'application/json'); - req.onreadystatechange = function() { - if (req.readyState === READYSTATE_COMPLETE && callback && typeof callback === 'function') { - try { - callback(params); - } catch (e) { - // TODO: Log this somehow (consider adding a logger to the EventDispatcher interface) - } - } - }; - req.send(JSON.stringify(params)); - } else { - // add param for cors headers to be sent by the log endpoint - url += '?wxhr=true'; - if (params) { - url += '&' + toQueryString(params); - } - - var req = new XMLHttpRequest(); - req.open(GET_METHOD, url, true); - req.onreadystatechange = function() { - if (req.readyState === READYSTATE_COMPLETE && callback && typeof callback === 'function') { - try { - callback(); - } catch (e) { - // TODO: Log this somehow (consider adding a logger to the EventDispatcher interface) - } - } - }; - req.send(); - } - }, -}; - -var toQueryString = function(obj) { - return fns.map(obj, function(v, k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(v); - }).join('&'); -}; diff --git a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.browser.tests.js b/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.browser.tests.js deleted file mode 100644 index a4b1294f3..000000000 --- a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.browser.tests.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var eventDispatcher = require('./index.browser'); -var chai = require('chai'); -var assert = chai.assert; -var sinon = require('sinon'); - -describe('lib/plugins/event_dispatcher/browser', function() { - describe('APIs', function() { - describe('dispatchEvent', function() { - var xhr; - var requests; - beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest = xhr; - requests = []; - xhr.onCreate = function (req) { - requests.push(req); - }; - }); - - afterEach(function() { - xhr.restore(); - }); - - it('should send a POST request with the specified params', function(done) { - var eventParams = {'testParam': 'testParamValue'}; - var eventObj = { - url: 'https://cdn.com/event', - body: { - id: 123, - }, - httpVerb: 'POST', - params: eventParams - }; - - var callback = sinon.spy(); - eventDispatcher.dispatchEvent(eventObj, callback); - assert.strictEqual(1, requests.length); - assert.strictEqual(requests[0].method, 'POST'); - assert.strictEqual(requests[0].requestBody, JSON.stringify(eventParams)); - done(); - }); - - it('should execute the callback passed to event dispatcher with a post', function(done) { - var eventParams = {'testParam': 'testParamValue'}; - var eventObj = { - url: 'https://cdn.com/event', - body: { - id: 123, - }, - httpVerb: 'POST', - params: eventParams - }; - - var callback = sinon.spy(); - eventDispatcher.dispatchEvent(eventObj, callback); - requests[ 0 ].respond([ 200, {}, '{"url":"https://cdn.com/event","body":{"id":123},"httpVerb":"POST","params":{"testParam":"testParamValue"}}' ]); - sinon.assert.calledOnce(callback); - done(); - }); - - it('should execute the callback passed to event dispatcher with a get', function(done) { - var eventObj = { - url: 'https://cdn.com/event', - httpVerb: 'GET' - }; - - var callback = sinon.spy(); - eventDispatcher.dispatchEvent(eventObj, callback); - requests[ 0 ].respond([ 200, {}, '{"url":"https://cdn.com/event","httpVerb":"GET"' ]); - sinon.assert.calledOnce(callback); - done(); - }); - - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.node.js b/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.node.js deleted file mode 100644 index 0b6e2b9cf..000000000 --- a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.node.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright 2016-2018, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var http = require('http'); -var https = require('https'); -var url = require('url'); - -module.exports = { - /** - * Dispatch an HTTP request to the given url and the specified options - * @param {Object} eventObj Event object containing - * @param {string} eventObj.url the url to make the request to - * @param {Object} eventObj.params parameters to pass to the request (i.e. in the POST body) - * @param {string} eventObj.httpVerb the HTTP request method type. only POST is supported. - * @param {function} callback callback to execute - * @return {ClientRequest|undefined} ClientRequest object which made the request, or undefined if no request was made (error) - */ - dispatchEvent: function(eventObj, callback) { - // Non-POST requests not supported - if (eventObj.httpVerb !== 'POST') { - return; - } - - var parsedUrl = url.parse(eventObj.url); - var path = parsedUrl.path; - if (parsedUrl.query) { - path += '?' + parsedUrl.query; - } - - var dataString = JSON.stringify(eventObj.params); - - var requestOptions = { - host: parsedUrl.host, - path: parsedUrl.path, - method: 'POST', - headers: { - 'content-type': 'application/json', - 'content-length': dataString.length.toString(), - } - }; - - var requestCallback = function(response) { - if (response && response.statusCode && response.statusCode >= 200 && response.statusCode < 400) { - callback(response); - } - }; - - var req = (parsedUrl.protocol === 'http:' ? http : https).request(requestOptions, requestCallback); - // Add no-op error listener to prevent this from throwing - req.on('error', function() {}); - req.write(dataString); - req.end(); - return req; - } -}; diff --git a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.node.tests.js b/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.node.tests.js deleted file mode 100644 index 82ec57ee5..000000000 --- a/packages/optimizely-sdk/lib/plugins/event_dispatcher/index.node.tests.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright 2016-2018, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var eventDispatcher = require('./index.node'); -var chai = require('chai'); -var assert = chai.assert; -var nock = require('nock'); -var sinon = require('sinon'); - -describe('lib/plugins/event_dispatcher/node', function() { - describe('APIs', function() { - describe('dispatchEvent', function() { - var stubCallback = { - callback: function() {} - }; - - beforeEach(function() { - sinon.stub(stubCallback, 'callback'); - nock('https://cdn.com') - .post('/event') - .reply(200, { - ok: true, - }); - }); - - afterEach(function() { - stubCallback.callback.restore(); - nock.cleanAll(); - }); - - it('should send a POST request with the specified params', function(done) { - var eventObj = { - url: 'https://cdn.com/event', - params: { - id: 123, - }, - httpVerb: 'POST', - }; - - eventDispatcher.dispatchEvent(eventObj, function(resp) { - assert.equal(200, resp.statusCode); - done(); - }); - }); - - it('should execute the callback passed to event dispatcher', function(done) { - var eventObj = { - url: 'https://cdn.com/event', - params: { - id: 123, - }, - httpVerb: 'POST', - }; - - eventDispatcher.dispatchEvent(eventObj, stubCallback.callback) - .on('response', function(response) { - sinon.assert.calledOnce(stubCallback.callback); - done(); - }) - .on('error', function(error) { - assert.fail('status code okay', 'status code not okay', ''); - }); - }); - - it('rejects GET httpVerb', function() { - var eventObj = { - url: 'https://cdn.com/event', - params: { - id: 123, - }, - httpVerb: 'GET', - }; - - var callback = sinon.spy(); - eventDispatcher.dispatchEvent(eventObj, callback); - sinon.assert.notCalled(callback); - }); - }); - - it('does not throw in the event of an error', function() { - var eventObj = { - url: 'https://example', - params: {}, - httpVerb: 'POST', - }; - - var callback = sinon.spy(); - eventDispatcher.dispatchEvent(eventObj, callback); - sinon.assert.notCalled(callback); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/plugins/logger/index.tests.js b/packages/optimizely-sdk/lib/plugins/logger/index.tests.js deleted file mode 100644 index 006d39252..000000000 --- a/packages/optimizely-sdk/lib/plugins/logger/index.tests.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright 2016, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var logger = require('./'); -var chai = require('chai'); -var enums = require('../../utils/enums'); -var assert = chai.assert; -var expect = chai.expect; -var sinon = require('sinon'); - -var LOG_LEVEL = enums.LOG_LEVEL; -describe('lib/plugins/logger', function() { - describe('APIs', function() { - var defaultLogger; - describe('createLogger', function() { - it('should return an instance of the default logger', function() { - defaultLogger = logger.createLogger({logLevel: LOG_LEVEL.NOTSET}); - assert.isObject(defaultLogger); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.NOTSET); - }); - }); - - describe('log', function() { - beforeEach(function() { - defaultLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - - sinon.stub(console, 'log'); - sinon.stub(console, 'info'); - sinon.stub(console, 'warn'); - sinon.stub(console, 'error'); - }); - - afterEach(function() { - console.log.restore(); - console.info.restore(); - console.warn.restore(); - console.error.restore(); - }); - - it('should log a message at the threshold log level', function() { - defaultLogger.log(LOG_LEVEL.INFO, 'message'); - - sinon.assert.notCalled(console.log); - sinon.assert.calledOnce(console.info); - sinon.assert.calledWithExactly(console.info, sinon.match(/.*INFO.*message.*/)); - sinon.assert.notCalled(console.warn); - sinon.assert.notCalled(console.error); - }); - - it('should log a message if its log level is higher than the threshold log level', function() { - defaultLogger.log(LOG_LEVEL.WARNING, 'message'); - - sinon.assert.notCalled(console.log); - sinon.assert.notCalled(console.info); - sinon.assert.calledOnce(console.warn); - sinon.assert.calledWithExactly(console.warn, sinon.match(/.*WARN.*message.*/)); - sinon.assert.notCalled(console.error); - }); - - it('should not log a message if its log level is lower than the threshold log level', function() { - defaultLogger.log(LOG_LEVEL.DEBUG, 'message'); - - sinon.assert.notCalled(console.log); - sinon.assert.notCalled(console.info); - sinon.assert.notCalled(console.warn); - sinon.assert.notCalled(console.error); - }); - }); - - describe('setLogLevel', function() { - beforeEach(function() { - defaultLogger = logger.createLogger({logLevel: LOG_LEVEL.NOTSET}); - }); - - it('should set the log level to the specified log level', function() { - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.NOTSET); - - defaultLogger.setLogLevel(LOG_LEVEL.DEBUG); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.DEBUG); - - defaultLogger.setLogLevel(LOG_LEVEL.INFO); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.INFO); - }); - - it('should set the log level to the ERROR when log level is not specified', function() { - defaultLogger.setLogLevel(); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.ERROR); - }); - - it('should set the log level to the ERROR when log level is not valid', function() { - defaultLogger.setLogLevel(-123); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.ERROR); - - defaultLogger.setLogLevel(undefined); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.ERROR); - - defaultLogger.setLogLevel('abc'); - expect(defaultLogger.logLevel).to.equal(LOG_LEVEL.ERROR); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/tests/test_data.js b/packages/optimizely-sdk/lib/tests/test_data.js deleted file mode 100644 index bfac5e239..000000000 --- a/packages/optimizely-sdk/lib/tests/test_data.js +++ /dev/null @@ -1,2312 +0,0 @@ -/** - * Copyright 2016-2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var cloneDeep = require('lodash/cloneDeep'); - -var config = { - revision: '42', - version: '2', - events: [ - { - key: 'testEvent', - experimentIds: ['111127'], - id: '111095' - }, - { - key: 'Total Revenue', - experimentIds: ['111127'], - id: '111096' - }, - { - key: 'testEventWithAudiences', - experimentIds: ['122227'], - id: '111097' - }, - { - key: 'testEventWithoutExperiments', - experimentIds: [], - id: '111098' - }, - { - key: 'testEventWithExperimentNotRunning', - experimentIds: ['133337'], - id: '111099' - }, - { - key: 'testEventWithMultipleExperiments', - experimentIds: ['111127', '122227', '133337'], - id: '111100' - }, - { - key: 'testEventLaunched', - experimentIds: ['144447'], - id: '111101' - } - ], - groups: [ - { - id: '666', - policy: 'random', - trafficAllocation: [{ - entityId: '442', - endOfRange: 3000 - }, { - entityId: '443', - endOfRange: 6000 - }], - experiments: [{ - id: '442', - key: 'groupExperiment1', - status: 'Running', - variations: [{ - id: '551', - key: 'var1exp1' - }, { - id: '552', - key: 'var2exp1' - }], - trafficAllocation: [{ - entityId: '551', - endOfRange: 5000 - }, { - entityId: '552', - endOfRange: 9000 - }, { - entityId: '', - endOfRange: 10000 - }], - audienceIds: ['11154'], - forcedVariations: {}, - layerId: '1' - }, { - id: '443', - key: 'groupExperiment2', - status: 'Running', - variations: [{ - id: '661', - key: 'var1exp2' - }, { - id: '662', - key: 'var2exp2' - }], - trafficAllocation: [{ - entityId: '661', - endOfRange: 5000 - }, { - entityId: '662', - endOfRange: 10000 - }], - audienceIds: [], - forcedVariations: {}, - layerId: '2' - }] - }, { - id: '667', - policy: 'overlapping', - trafficAllocation: [], - experiments: [{ - id: '444', - key: 'overlappingGroupExperiment1', - status: 'Running', - variations: [{ - id: '553', - key: 'overlappingvar1' - }, { - id: '554', - key: 'overlappingvar2' - }], - trafficAllocation: [{ - entityId: '553', - endOfRange: 1500 - }, { - entityId: '554', - endOfRange: 3000 - }], - audienceIds: [], - forcedVariations: {}, - layerId: '3' - }] - } - ], - experiments: [ - { - key: 'testExperiment', - status: 'Running', - forcedVariations: { - 'user1': 'control', - 'user2': 'variation' - }, - audienceIds: [], - layerId: '4', - trafficAllocation: [{ - entityId: '111128', - endOfRange: 4000 - }, { - entityId: '111129', - endOfRange: 9000 - }], - id: '111127', - variations: [{ - key: 'control', - id: '111128' - }, { - key: 'variation', - id: '111129' - }] - }, { - key: 'testExperimentWithAudiences', - status: 'Running', - forcedVariations: { - 'user1': 'controlWithAudience', - 'user2': 'variationWithAudience' - }, - audienceIds: ['11154'], - layerId: '5', - trafficAllocation: [{ - entityId: '122228', - endOfRange: 4000, - }, { - entityId: '122229', - endOfRange: 10000 - }], - id: '122227', - variations: [{ - key: 'controlWithAudience', - id: '122228' - }, { - key: 'variationWithAudience', - id: '122229' - }] - }, { - key: 'testExperimentNotRunning', - status: 'Not started', - forcedVariations: { - 'user1': 'controlNotRunning', - 'user2': 'variationNotRunning' - }, - audienceIds: [], - layerId: '6', - trafficAllocation: [{ - entityId: '133338', - endOfRange: 4000 - }, { - entityId: '133339', - endOfRange: 10000 - }], - id: '133337', - variations: [{ - key: 'controlNotRunning', - id: '133338' - }, { - key: 'variationNotRunning', - id: '133339' - }] - }, { - key: 'testExperimentLaunched', - status: 'Launched', - forcedVariations: {}, - audienceIds: [], - layerId: '7', - trafficAllocation: [{ - entityId: '144448', - endOfRange: 5000, - }, { - entityId: '144449', - endOfRange: 10000 - }], - id: '144447', - variations: [{ - key: 'controlLaunched', - id: '144448' - }, { - key: 'variationLaunched', - id: '144449' - }] - }], - accountId: '12001', - attributes: [{ - key: 'browser_type', - id: '111094' - }, { - id: "323434545", - key: "boolean_key" - }, { - id: "616727838", - key: "integer_key" - }, { - id: "808797686", - key: "double_key" - }, { - id: "808797687", - key: "valid_positive_number" - }, { - id: "808797688", - key: "valid_negative_number" - }, { - id: "808797689", - key: "invalid_number" - }, { - id: "808797690", - key: "array" - } - ], - audiences: [{ - name: 'Firefox users', - conditions: '["and", ["or", ["or", {"name": "browser_type", "type": "custom_attribute", "value": "firefox"}]]]', - id: '11154' - }], - projectId: '111001' -}; - -var getParsedAudiences = [{ - name: 'Firefox users', - conditions: ['and', ['or', ['or', {'name': 'browser_type', 'type': 'custom_attribute', 'value': 'firefox'}]]], - id: '11154' -}]; - -var getTestProjectConfig = function() { - return cloneDeep(config); -}; - -var configWithFeatures = { - 'events': [ - { - 'key': 'item_bought', - 'id': '594089', - 'experimentIds': [ - '594098', - '595010', - '599028', - '599082' - ] - } - ], - 'featureFlags': [ - { - 'rolloutId': '594030', - 'key': 'test_feature', - 'id': '594021', - 'experimentIds': [], - 'variables': [ - { - 'type': 'boolean', - 'key': 'new_content', - 'id': '4919852825313280', - 'defaultValue': 'false' - }, - { - 'type': 'integer', - 'key': 'lasers', - 'id': '5482802778734592', - 'defaultValue': '400' - }, - { - 'type': 'double', - 'key': 'price', - 'id': '6045752732155904', - 'defaultValue': '14.99' - }, - { - 'type': 'string', - 'key': 'message', - 'id': '6327227708866560', - 'defaultValue': 'Hello' - } - ] - }, - { - 'rolloutId': '594059', - 'key': 'test_feature_2', - 'id': '594050', - 'experimentIds': [], - 'variables': [ - { - 'type': 'double', - 'key': 'miles_to_the_wall', - 'id': '5060590313668608', - 'defaultValue': '30.34' - }, - { - 'type': 'string', - 'key': 'motto', - 'id': '5342065290379264', - 'defaultValue': 'Winter is coming' - }, - { - 'type': 'integer', - 'key': 'soldiers_available', - 'id': '6186490220511232', - 'defaultValue': '1000' - }, - { - 'type': 'boolean', - 'key': 'is_winter_coming', - 'id': '6467965197221888', - 'defaultValue': 'true' - } - ] - }, - { - 'rolloutId': '', - 'key': 'test_feature_for_experiment', - 'id': '594081', - 'experimentIds': [ - '594098' - ], - 'variables': [ - { - 'type': 'integer', - 'key': 'num_buttons', - 'id': '4792309476491264', - 'defaultValue': '10' - }, - { - 'type': 'boolean', - 'key': 'is_button_animated', - 'id': '5073784453201920', - 'defaultValue': 'false' - }, - { - 'type': 'string', - 'key': 'button_txt', - 'id': '5636734406623232', - 'defaultValue': 'Buy me' - }, - { - 'type': 'double', - 'key': 'button_width', - 'id': '6199684360044544', - 'defaultValue': '50.55' - } - ] - }, - { - 'rolloutId': '', - 'key': 'feature_with_group', - 'id': '595001', - 'experimentIds': [ - '595010' - ], - 'variables': [] - }, - { - 'rolloutId': '599055', - 'key': 'shared_feature', - 'id': '599011', - 'experimentIds': [ - '599028' - ], - 'variables': [ - { - 'type': 'integer', - 'key': 'lasers', - 'id': '4937719889264640', - 'defaultValue': '100' - }, - { - 'type': 'string', - 'key': 'message', - 'id': '6345094772817920', - 'defaultValue': 'shared' - } - ] - }, - { - 'rolloutId': '', - 'key': 'unused_flag', - 'id': '599110', - 'experimentIds': [], - 'variables': [] - }, - { - 'rolloutId': '', - 'key': 'feature_exp_no_traffic', - 'id': '4482920079', - 'experimentIds': [ - '12115595439' - ], - 'variables': [] - } - ], - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '594096' - }, - { - 'endOfRange': 10000, - 'entityId': '594097' - } - ], - 'layerId': '594093', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': 'variation', - 'id': '594096', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4792309476491264', - 'value': '2' - }, - { - 'id': '5073784453201920', - 'value': 'true' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me NOW' - }, - { - 'id': '6199684360044544', - 'value': '20.25' - } - ] - }, - { - 'key': 'control', - 'id': '594097', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4792309476491264', - 'value': '10' - }, - { - 'id': '5073784453201920', - 'value': 'false' - }, - { - 'id': '5636734406623232', - 'value': 'Buy me' - }, - { - 'id': '6199684360044544', - 'value': '50.55' - } - ] - } - ], - 'status': 'Running', - 'key': 'testing_my_feature', - 'id': '594098' - }, - { - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '599026' - }, - { - 'endOfRange': 10000, - 'entityId': '599027' - } - ], - 'layerId': '599023', - 'forcedVariations': {}, - 'audienceIds': [ - '594017' - ], - 'variations': [ - { - 'key': 'treatment', - 'id': '599026', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '100' - }, - { - 'id': '6345094772817920', - 'value': 'shared' - } - ] - }, - { - 'key': 'control', - 'id': '599027', - 'featureEnabled': false, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '100' - }, - { - 'id': '6345094772817920', - 'value': 'shared' - } - ] - } - ], - 'status': 'Running', - 'key': 'test_shared_feature', - 'id': '599028' - } - ], - 'anonymizeIP': true, - 'botFiltering': true, - 'audiences': [ - { - 'id': '594017', - 'name': 'test_audience', - 'conditions': '["and", ["or", ["or", {"type": "custom_attribute", "name": "test_attribute", "value": "test_value"}]]]' - } - ], - 'revision': '35', - 'groups': [ - { - 'policy': 'random', - 'id': '595024', - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '595008' - }, - { - 'endOfRange': 10000, - 'entityId': '595009' - } - ], - 'layerId': '595005', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': 'var', - 'id': '595008', - 'variables': [] - }, - { - 'key': 'con', - 'id': '595009', - 'variables': [] - } - ], - 'status': 'Running', - 'key': 'exp_with_group', - 'id': '595010' - }, - { - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '599080' - }, - { - 'endOfRange': 10000, - 'entityId': '599081' - } - ], - 'layerId': '599077', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': 'treatment', - 'id': '599080', - 'variables': [] - }, - { - 'key': 'control', - 'id': '599081', - 'variables': [] - } - ], - 'status': 'Running', - 'key': 'other_exp_with_grup', - 'id': '599082' - } - ], - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '595010' - }, - { - 'endOfRange': 10000, - 'entityId': '599082' - } - ] - }, - { - 'policy': 'random', - 'id': '595025', - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '12098126627' - } - ], - 'layerId': '595005', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': 'all_traffic_variation', - 'id': '12098126627', - 'variables': [] - }, - { - 'key': 'no_traffic_variation', - 'id': '12098126628', - 'variables': [] - } - ], - 'status': 'Running', - 'key': 'all_traffic_experiment', - 'id': '12198292375' - }, - { - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '12098126629' - }, - { - 'endOfRange': 10000, - 'entityId': '12098126630' - } - ], - 'layerId': '12187694826', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': 'variation_5000', - 'id': '12098126629', - 'variables': [] - }, - { - 'key': 'variation_10000', - 'id': '12098126630', - 'variables': [] - } - ], - 'status': 'Running', - 'key': 'no_traffic_experiment', - 'id': '12115595439' - } - ], - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '12198292375' - } - ] - } - ], - 'attributes': [ - { - 'key': 'test_attribute', - 'id': '594014' - } - ], - 'rollouts': [ - { - 'id': '594030', - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 5000, - 'entityId': '594032' - } - ], - 'layerId': '594030', - 'forcedVariations': {}, - 'audienceIds': [ - '594017' - ], - 'variations': [ - { - 'key': '594032', - 'id': '594032', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'true' - }, - { - 'id': '5482802778734592', - 'value': '395' - }, - { - 'id': '6045752732155904', - 'value': '4.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello audience' - } - ] - } - ], - 'status': 'Not started', - 'key': '594031', - 'id': '594031' - }, - { - 'trafficAllocation': [ - { - 'endOfRange': 0, - 'entityId': '594038' - } - ], - 'layerId': '594030', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': '594038', - 'id': '594038', - 'featureEnabled': false, - 'variables': [ - { - 'id': '4919852825313280', - 'value': 'false' - }, - { - 'id': '5482802778734592', - 'value': '400' - }, - { - 'id': '6045752732155904', - 'value': '14.99' - }, - { - 'id': '6327227708866560', - 'value': 'Hello' - } - ] - } - ], - 'status': 'Not started', - 'key': '594037', - 'id': '594037' - } - ] - }, - { - 'id': '594059', - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '594061' - } - ], - 'layerId': '594059', - 'forcedVariations': {}, - 'audienceIds': [ - '594017' - ], - 'variations': [ - { - 'key': '594061', - 'id': '594061', - 'featureEnabled': true, - 'variables': [ - { - 'id': '5060590313668608', - 'value': '27.34' - }, - { - 'id': '5342065290379264', - 'value': 'Winter is NOT coming' - }, - { - 'id': '6186490220511232', - 'value': '10003' - }, - { - 'id': '6467965197221888', - 'value': 'false' - } - ] - } - ], - 'status': 'Not started', - 'key': '594060', - 'id': '594060' - }, - { - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '594067' - } - ], - 'layerId': '594059', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': '594067', - 'id': '594067', - 'featureEnabled': true, - 'variables': [ - { - 'id': '5060590313668608', - 'value': '30.34' - }, - { - 'id': '5342065290379264', - 'value': 'Winter is coming definitely' - }, - { - 'id': '6186490220511232', - 'value': '500' - }, - { - 'id': '6467965197221888', - 'value': 'true' - } - ] - } - ], - 'status': 'Not started', - 'key': '594066', - 'id': '594066' - } - ] - }, - { - 'id': '599055', - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '599057' - } - ], - 'layerId': '599055', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': '599057', - 'id': '599057', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '200' - }, - { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - } - ] - } - ], - 'status': 'Not started', - 'key': '599056', - 'id': '599056' - } - ] - } - ], - 'projectId': '594001', - 'accountId': '572018', - 'version': '4', - 'variables': [] -}; - -var getTestProjectConfigWithFeatures = function() { - return cloneDeep(configWithFeatures); -}; - -var datafileWithFeaturesExpectedData = { - rolloutIdMap: { - 599055: { - 'id': '599055', - 'experiments': [ - { - 'trafficAllocation': [ - { - 'endOfRange': 10000, - 'entityId': '599057' - } - ], - 'layerId': '599055', - 'forcedVariations': {}, - 'audienceIds': [], - 'variations': [ - { - 'key': '599057', - 'id': '599057', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '200' - }, - { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - } - ] - } - ], - 'status': 'Not started', - 'key': '599056', - 'id': '599056', - variationKeyMap: { - 599057: { - 'key': '599057', - 'id': '599057', - 'featureEnabled': true, - 'variables': [ - { - 'id': '4937719889264640', - 'value': '200' - }, - { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - }, - ], - }, - }, - }, - ], - }, - 594030: { - experiments: [ - { - 'audienceIds': [ - '594017' - ], - 'status': 'Not started', - 'layerId': '594030', - 'forcedVariations': {}, - 'variations': [ - { - 'variables': [ - { - 'value': 'true', - 'id': '4919852825313280' - }, - { - 'value': '395', - 'id': '5482802778734592' - }, - { - 'value': '4.99', - 'id': '6045752732155904' - }, - { - 'value': 'Hello audience', - 'id': '6327227708866560' - } - ], - 'featureEnabled': true, - 'key': '594032', - 'id': '594032' - } - ], - 'trafficAllocation': [ - { - 'entityId': '594032', - 'endOfRange': 5000 - } - ], - 'key': '594031', - 'id': '594031', - variationKeyMap: { - 594032: { - 'variables': [ - { - 'value': 'true', - 'id': '4919852825313280' - }, - { - 'value': '395', - 'id': '5482802778734592' - }, - { - 'value': '4.99', - 'id': '6045752732155904' - }, - { - 'value': 'Hello audience', - 'id': '6327227708866560' - } - ], - 'featureEnabled': true, - 'key': '594032', - 'id': '594032' - } - }, - }, - { - 'audienceIds': [], - 'status': 'Not started', - 'layerId': '594030', - 'forcedVariations': {}, - 'variations': [ - { - 'variables': [ - { - 'value': 'false', - 'id': '4919852825313280' - }, - { - 'value': '400', - 'id': '5482802778734592' - }, - { - 'value': '14.99', - 'id': '6045752732155904' - }, - { - 'value': 'Hello', - 'id': '6327227708866560' - } - ], - 'featureEnabled': false, - 'key': '594038', - 'id': '594038' - } - ], - 'trafficAllocation': [ - { - 'entityId': '594038', - 'endOfRange': 0 - } - ], - 'key': '594037', - 'id': '594037', - variationKeyMap: { - 594038: { - 'variables': [ - { - 'value': 'false', - 'id': '4919852825313280' - }, - { - 'value': '400', - 'id': '5482802778734592' - }, - { - 'value': '14.99', - 'id': '6045752732155904' - }, - { - 'value': 'Hello', - 'id': '6327227708866560' - } - ], - 'featureEnabled': false, - 'key': '594038', - 'id': '594038' - }, - }, - } - ], - id: '594030', - }, - 594059: { - experiments: [ - { - 'audienceIds': [ - '594017' - ], - 'status': 'Not started', - 'layerId': '594059', - 'forcedVariations': {}, - 'variations': [ - { - 'variables': [ - { - 'value': '27.34', - 'id': '5060590313668608' - }, - { - 'value': 'Winter is NOT coming', - 'id': '5342065290379264' - }, - { - 'value': '10003', - 'id': '6186490220511232' - }, - { - 'value': 'false', - 'id': '6467965197221888' - } - ], - 'featureEnabled': true, - 'key': '594061', - 'id': '594061' - } - ], - 'trafficAllocation': [ - { - 'entityId': '594061', - 'endOfRange': 10000 - } - ], - 'key': '594060', - 'id': '594060', - variationKeyMap: { - 594061: { - 'variables': [ - { - 'value': '27.34', - 'id': '5060590313668608' - }, - { - 'value': 'Winter is NOT coming', - 'id': '5342065290379264' - }, - { - 'value': '10003', - 'id': '6186490220511232' - }, - { - 'value': 'false', - 'id': '6467965197221888' - } - ], - 'featureEnabled': true, - 'key': '594061', - 'id': '594061' - }, - }, - }, - { - 'audienceIds': [], - 'status': 'Not started', - 'layerId': '594059', - 'forcedVariations': {}, - 'variations': [ - { - 'variables': [ - { - 'value': '30.34', - 'id': '5060590313668608' - }, - { - 'value': 'Winter is coming definitely', - 'id': '5342065290379264' - }, - { - 'value': '500', - 'id': '6186490220511232' - }, - { - 'value': 'true', - 'id': '6467965197221888' - } - ], - 'featureEnabled': true, - 'key': '594067', - 'id': '594067' - } - ], - 'trafficAllocation': [ - { - 'entityId': '594067', - 'endOfRange': 10000 - } - ], - 'key': '594066', - 'id': '594066', - variationKeyMap: { - 594067: { - 'variables': [ - { - 'value': '30.34', - 'id': '5060590313668608' - }, - { - 'value': 'Winter is coming definitely', - 'id': '5342065290379264' - }, - { - 'value': '500', - 'id': '6186490220511232' - }, - { - 'value': 'true', - 'id': '6467965197221888' - } - ], - 'featureEnabled': true, - 'key': '594067', - 'id': '594067' - }, - }, - }, - ], - id: '594059', - }, - }, - - variationVariableUsageMap: { - 594032: { - 4919852825313280: { - id: '4919852825313280', - value: 'true', - }, - 5482802778734592: { - id: '5482802778734592', - value: '395', - }, - 6045752732155904: { - id: '6045752732155904', - value: '4.99', - }, - 6327227708866560: { - id: '6327227708866560', - value: 'Hello audience', - }, - }, - 594038: { - 4919852825313280: { - id: '4919852825313280', - value: 'false', - }, - 5482802778734592: { - id: '5482802778734592', - value: '400', - }, - 6045752732155904: { - id: '6045752732155904', - value: '14.99', - }, - 6327227708866560: { - id: '6327227708866560', - value: 'Hello', - }, - }, - 594061: { - 5060590313668608: { - id: '5060590313668608', - value: '27.34', - }, - 5342065290379264: { - id: '5342065290379264', - value: 'Winter is NOT coming', - }, - 6186490220511232: { - id: '6186490220511232', - value: '10003', - }, - 6467965197221888: { - id: '6467965197221888', - value: 'false', - }, - }, - 594067: { - 5060590313668608: { - id: '5060590313668608', - value: '30.34', - }, - 5342065290379264: { - id: '5342065290379264', - value: 'Winter is coming definitely', - }, - 6186490220511232: { - id: '6186490220511232', - value: '500', - }, - 6467965197221888: { - id: '6467965197221888', - value: 'true', - }, - }, - 594096: { - 4792309476491264: { - 'value': '2', - 'id': '4792309476491264', - }, - 5073784453201920: { - 'value': 'true', - 'id': '5073784453201920' - }, - 5636734406623232: { - 'value': 'Buy me NOW', - 'id': '5636734406623232' - }, - 6199684360044544: { - 'value': '20.25', - 'id': '6199684360044544' - }, - }, - 594097: { - 4792309476491264: { - 'value': '10', - 'id': '4792309476491264' - }, - 5073784453201920: { - 'value': 'false', - 'id': '5073784453201920' - }, - 5636734406623232: { - 'value': 'Buy me', - 'id': '5636734406623232' - }, - 6199684360044544: { - 'value': '50.55', - 'id': '6199684360044544' - }, - }, - 595008: {}, - 595009: {}, - 599026: { - 4937719889264640: { - 'id': '4937719889264640', - 'value': '100' - }, - 6345094772817920: { - 'id': '6345094772817920', - 'value': 'shared' - }, - }, - 599027: { - 4937719889264640: { - 'id': '4937719889264640', - 'value': '100' - }, - 6345094772817920: { - 'id': '6345094772817920', - 'value': 'shared' - }, - }, - 599057: { - 4937719889264640: { - 'id': '4937719889264640', - 'value': '200' - }, - 6345094772817920: { - 'id': '6345094772817920', - 'value': 'i\'m a rollout' - }, - }, - 599080: {}, - 599081: {}, - 12098126627: {}, - 12098126628: {}, - 12098126629: {}, - 12098126630: {}, - }, - - featureKeyMap: { - test_feature: { - 'variables': [{ - 'defaultValue': 'false', - 'key': 'new_content', - 'type': 'boolean', - 'id': '4919852825313280' - }, { - 'defaultValue': '400', - 'key': 'lasers', - 'type': 'integer', - 'id': '5482802778734592' - }, { - 'defaultValue': '14.99', - 'key': 'price', - 'type': 'double', - 'id': '6045752732155904' - }, { - 'defaultValue': 'Hello', - 'key': 'message', - 'type': 'string', - 'id': '6327227708866560' - }], - 'experimentIds': [], - 'rolloutId': '594030', - 'key': 'test_feature', - 'id': '594021', - variableKeyMap: { - new_content: { - 'defaultValue': 'false', - 'key': 'new_content', - 'type': 'boolean', - 'id': '4919852825313280' - }, - lasers: { - 'defaultValue': '400', - 'key': 'lasers', - 'type': 'integer', - 'id': '5482802778734592' - }, - price: { - 'defaultValue': '14.99', - 'key': 'price', - 'type': 'double', - 'id': '6045752732155904' - }, - message: { - 'defaultValue': 'Hello', - 'key': 'message', - 'type': 'string', - 'id': '6327227708866560' - }, - }, - }, - test_feature_2: { - 'variables': [{ - 'defaultValue': '30.34', - 'key': 'miles_to_the_wall', - 'type': 'double', - 'id': '5060590313668608' - }, { - 'defaultValue': 'Winter is coming', - 'key': 'motto', - 'type': 'string', - 'id': '5342065290379264' - }, { - 'defaultValue': '1000', - 'key': 'soldiers_available', - 'type': 'integer', - 'id': '6186490220511232' - }, { - 'defaultValue': 'true', - 'key': 'is_winter_coming', - 'type': 'boolean', - 'id': '6467965197221888', - }], - 'experimentIds': [], - 'rolloutId': '594059', - 'key': 'test_feature_2', - 'id': '594050', - variableKeyMap: { - miles_to_the_wall: { - 'defaultValue': '30.34', - 'key': 'miles_to_the_wall', - 'type': 'double', - 'id': '5060590313668608' - }, - motto: { - 'defaultValue': 'Winter is coming', - 'key': 'motto', - 'type': 'string', - 'id': '5342065290379264' - }, - soldiers_available: { - 'defaultValue': '1000', - 'key': 'soldiers_available', - 'type': 'integer', - 'id': '6186490220511232' - }, - is_winter_coming: { - 'defaultValue': 'true', - 'key': 'is_winter_coming', - 'type': 'boolean', - 'id': '6467965197221888', - }, - } - }, - test_feature_for_experiment: { - 'variables': [{ - 'defaultValue': '10', - 'key': 'num_buttons', - 'type': 'integer', - 'id': '4792309476491264' - }, { - 'defaultValue': 'false', - 'key': 'is_button_animated', - 'type': 'boolean', - 'id': '5073784453201920' - }, { - 'defaultValue': 'Buy me', - 'key': 'button_txt', - 'type': 'string', - 'id': '5636734406623232' - }, { - 'defaultValue': '50.55', - 'key': 'button_width', - 'type': 'double', - 'id': '6199684360044544', - }], - 'experimentIds': ['594098'], - 'rolloutId': '', - 'key': 'test_feature_for_experiment', - 'id': '594081', - variableKeyMap: { - num_buttons: { - 'defaultValue': '10', - 'key': 'num_buttons', - 'type': 'integer', - 'id': '4792309476491264' - }, - is_button_animated: { - 'defaultValue': 'false', - 'key': 'is_button_animated', - 'type': 'boolean', - 'id': '5073784453201920' - }, - button_txt: { - 'defaultValue': 'Buy me', - 'key': 'button_txt', - 'type': 'string', - 'id': '5636734406623232' - }, - button_width: { - 'defaultValue': '50.55', - 'key': 'button_width', - 'type': 'double', - 'id': '6199684360044544', - }, - }, - }, - // This feature should have a groupId assigned because its experiment is in a group - feature_with_group: { - 'variables': [], - 'rolloutId': '', - 'experimentIds': ['595010'], - 'key': 'feature_with_group', - 'id': '595001', - variableKeyMap: {}, - groupId: '595024', - }, - shared_feature: { - 'rolloutId': '599055', - 'key': 'shared_feature', - 'id': '599011', - 'experimentIds': ['599028'], - 'variables': [ - { - 'type': 'integer', - 'key': 'lasers', - 'id': '4937719889264640', - 'defaultValue': '100' - }, - { - 'type': 'string', - 'key': 'message', - 'id': '6345094772817920', - 'defaultValue': 'shared' - } - ], - variableKeyMap: { - message: { - 'type': 'string', - 'key': 'message', - 'id': '6345094772817920', - 'defaultValue': 'shared' - }, - lasers: { - 'type': 'integer', - 'key': 'lasers', - 'id': '4937719889264640', - 'defaultValue': '100' - } - } - }, - unused_flag: { - 'rolloutId': '', - 'key': 'unused_flag', - 'id': '599110', - 'experimentIds': [], - 'variables': [], - variableKeyMap: {}, - }, - feature_exp_no_traffic: { - 'rolloutId': '', - 'key': 'feature_exp_no_traffic', - 'id': '4482920079', - 'experimentIds': ['12115595439'], - 'variables': [], - variableKeyMap: {}, - groupId: '595025', - }, - }, -}; - -var unsupportedVersionConfig = { - revision: '42', - version: '5', - events: [ - { - key: 'testEvent', - experimentIds: ['111127'], - id: '111095' - }, - { - key: 'Total Revenue', - experimentIds: ['111127'], - id: '111096' - }, - { - key: 'testEventWithAudiences', - experimentIds: ['122227'], - id: '111097' - }, - { - key: 'testEventWithoutExperiments', - experimentIds: [], - id: '111098' - }, - { - key: 'testEventWithExperimentNotRunning', - experimentIds: ['133337'], - id: '111099' - }, - { - key: 'testEventWithMultipleExperiments', - experimentIds: ['111127', '122227', '133337'], - id: '111100' - }, - { - key: 'testEventLaunched', - experimentIds: ['144447'], - id: '111101' - } - ], - groups: [ - { - id: '666', - policy: 'random', - trafficAllocation: [{ - entityId: '442', - endOfRange: 3000 - }, { - entityId: '443', - endOfRange: 6000 - }], - experiments: [{ - id: '442', - key: 'groupExperiment1', - status: 'Running', - variations: [{ - id: '551', - key: 'var1exp1' - }, { - id: '552', - key: 'var2exp1' - }], - trafficAllocation: [{ - entityId: '551', - endOfRange: 5000 - }, { - entityId: '552', - endOfRange: 9000 - }, { - entityId: '', - endOfRange: 10000 - }], - audienceIds: ['11154'], - forcedVariations: {}, - layerId: '1' - }, { - id: '443', - key: 'groupExperiment2', - status: 'Running', - variations: [{ - id: '661', - key: 'var1exp2' - }, { - id: '662', - key: 'var2exp2' - }], - trafficAllocation: [{ - entityId: '661', - endOfRange: 5000 - }, { - entityId: '662', - endOfRange: 10000 - }], - audienceIds: [], - forcedVariations: {}, - layerId: '2' - }] - }, { - id: '667', - policy: 'overlapping', - trafficAllocation: [], - experiments: [{ - id: '444', - key: 'overlappingGroupExperiment1', - status: 'Running', - variations: [{ - id: '553', - key: 'overlappingvar1' - }, { - id: '554', - key: 'overlappingvar2' - }], - trafficAllocation: [{ - entityId: '553', - endOfRange: 1500 - }, { - entityId: '554', - endOfRange: 3000 - }], - audienceIds: [], - forcedVariations: {}, - layerId: '3' - }] - } - ], - experiments: [ - { - key: 'testExperiment', - status: 'Running', - forcedVariations: { - 'user1': 'control', - 'user2': 'variation' - }, - audienceIds: [], - layerId: '4', - trafficAllocation: [{ - entityId: '111128', - endOfRange: 4000 - }, { - entityId: '111129', - endOfRange: 9000 - }], - id: '111127', - variations: [{ - key: 'control', - id: '111128' - }, { - key: 'variation', - id: '111129' - }] - }, { - key: 'testExperimentWithAudiences', - status: 'Running', - forcedVariations: { - 'user1': 'controlWithAudience', - 'user2': 'variationWithAudience' - }, - audienceIds: ['11154'], - layerId: '5', - trafficAllocation: [{ - entityId: '122228', - endOfRange: 4000, - }, { - entityId: '122229', - endOfRange: 10000 - }], - id: '122227', - variations: [{ - key: 'controlWithAudience', - id: '122228' - }, { - key: 'variationWithAudience', - id: '122229' - }] - }, { - key: 'testExperimentNotRunning', - status: 'Not started', - forcedVariations: { - 'user1': 'controlNotRunning', - 'user2': 'variationNotRunning' - }, - audienceIds: [], - layerId: '6', - trafficAllocation: [{ - entityId: '133338', - endOfRange: 4000 - }, { - entityId: '133339', - endOfRange: 10000 - }], - id: '133337', - variations: [{ - key: 'controlNotRunning', - id: '133338' - }, { - key: 'variationNotRunning', - id: '133339' - }] - }, { - key: 'testExperimentLaunched', - status: 'Launched', - forcedVariations: {}, - audienceIds: [], - layerId: '7', - trafficAllocation: [{ - entityId: '144448', - endOfRange: 5000, - }, { - entityId: '144449', - endOfRange: 10000 - }], - id: '144447', - variations: [{ - key: 'controlLaunched', - id: '144448' - }, { - key: 'variationLaunched', - id: '144449' - }] - }], - accountId: '12001', - attributes: [{ - key: 'browser_type', - id: '111094' - } - ], - audiences: [{ - name: 'Firefox users', - conditions: '["and", ["or", ["or", {"name": "browser_type", "type": "custom_attribute", "value": "firefox"}]]]', - id: '11154' - }], - projectId: '111001' -}; - -var getUnsupportedVersionConfig = function() { - return cloneDeep(unsupportedVersionConfig); -}; - -var typedAudiencesConfig = { - 'version': '4', - 'rollouts': [ - { - 'experiments': [ - { - 'status': 'Running', - 'key': '11488548027', - 'layerId': '11551226731', - 'trafficAllocation': [ - { - 'entityId': '11557362669', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], - 'variations': [ - { - 'variables': [], - 'id': '11557362669', - 'key': '11557362669', - 'featureEnabled': true - } - ], - 'forcedVariations': {}, - 'id': '11488548027' - } - ], - 'id': '11551226731' - }, - { - 'experiments': [ - { - 'status': 'Paused', - 'key': '11630490911', - 'layerId': '11638870867', - 'trafficAllocation': [ - { - 'entityId': '11475708558', - 'endOfRange': 0 - } - ], - 'audienceIds': [], - 'variations': [ - { - 'variables': [], - 'id': '11475708558', - 'key': '11475708558', - 'featureEnabled': false - } - ], - 'forcedVariations': {}, - 'id': '11630490911' - } - ], - 'id': '11638870867' - }, - { - 'experiments': [ - { - 'status': 'Running', - 'key': '11488548028', - 'layerId': '11551226732', - 'trafficAllocation': [ - { - 'entityId': '11557362670', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['0'], - 'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']], - 'variations': [ - { - 'variables': [], - 'id': '11557362670', - 'key': '11557362670', - 'featureEnabled': true - } - ], - 'forcedVariations': {}, - 'id': '11488548028' - } - ], - 'id': '11551226732' - }, - { - 'experiments': [ - { - 'status': 'Paused', - 'key': '11630490912', - 'layerId': '11638870868', - 'trafficAllocation': [ - { - 'entityId': '11475708559', - 'endOfRange': 0 - } - ], - 'audienceIds': [], - 'variations': [ - { - 'variables': [], - 'id': '11475708559', - 'key': '11475708559', - 'featureEnabled': false - } - ], - 'forcedVariations': {}, - 'id': '11630490912' - } - ], - 'id': '11638870868' - } - ], - 'anonymizeIP': false, - 'projectId': '11624721371', - 'variables': [], - 'featureFlags': [ - { - 'experimentIds': [], - 'rolloutId': '11551226731', - 'variables': [], - 'id': '11477755619', - 'key': 'feat' - }, - { - 'experimentIds': [ - '11564051718' - ], - 'rolloutId': '11638870867', - 'variables': [ - { - 'defaultValue': 'x', - 'type': 'string', - 'id': '11535264366', - 'key': 'x' - } - ], - 'id': '11567102051', - 'key': 'feat_with_var' - }, - { - 'experimentIds': [], - 'rolloutId': '11551226732', - 'variables': [], - 'id': '11567102052', - 'key': 'feat2' - }, - { - 'experimentIds': ['1323241599'], - 'rolloutId': '11638870868', - 'variables': [ - { - 'defaultValue': '10', - 'type': 'integer', - 'id': '11535264367', - 'key': 'z' - } - ], - 'id': '11567102053', - 'key': 'feat2_with_var' - }, - ], - 'experiments': [ - { - 'status': 'Running', - 'key': 'feat_with_var_test', - 'layerId': '11504144555', - 'trafficAllocation': [ - { - 'entityId': '11617170975', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], - 'variations': [ - { - 'variables': [ - { - 'id': '11535264366', - 'value': 'xyz' - } - ], - 'id': '11617170975', - 'key': 'variation_2', - 'featureEnabled': true - } - ], - 'forcedVariations': {}, - 'id': '11564051718' - }, - { - 'id': '1323241597', - 'key': 'typed_audience_experiment', - 'layerId': '1630555627', - 'status': 'Running', - 'variations': [ - { - 'id': '1423767503', - 'key': 'A', - 'variables': [] - } - ], - 'trafficAllocation': [ - { - 'entityId': '1423767503', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], - 'forcedVariations': {} - }, - { - 'id': '1323241598', - 'key': 'audience_combinations_experiment', - 'layerId': '1323241598', - 'status': 'Running', - 'variations': [ - { - 'id': '1423767504', - 'key': 'A', - 'variables': [] - } - ], - 'trafficAllocation': [ - { - 'entityId': '1423767504', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['0'], - 'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']], - 'forcedVariations': {} - }, - { - 'id': '1323241599', - 'key': 'feat2_with_var_test', - 'layerId': '1323241600', - 'status': 'Running', - 'variations': [ - { - 'variables': [ - { - 'id': '11535264367', - 'value': '150' - } - ], - 'id': '1423767505', - 'key': 'variation_2', - 'featureEnabled': true - } - ], - 'trafficAllocation': [ - { - 'entityId': '1423767505', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['0'], - 'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']], - 'forcedVariations': {} - }, - ], - 'audiences': [ - { - 'id': '3468206642', - 'name': 'exactString', - 'conditions': '["and", ["or", ["or", {"name": "house", "type": "custom_attribute", "value": "Gryffindor"}]]]' - }, - { - 'id': '3988293898', - 'name': '$$dummySubstringString', - 'conditions': '["and", ["or"]]', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3988293899', - 'name': '$$dummyExists', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206646', - 'name': '$$dummyExactNumber', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206647', - 'name': '$$dummyGtNumber', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206644', - 'name': '$$dummyLtNumber', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206643', - 'name': '$$dummyExactBoolean', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '0', - 'name': '$$dummy', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', - } - ], - 'typedAudiences': [ - { - 'id': '3988293898', - 'name': 'substringString', - 'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute', 'match':'substring', 'value':'Slytherin'}]]] - }, - { - 'id': '3988293899', - 'name': 'exists', - 'conditions': ['and', ['or', ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute', 'match':'exists'}]]] - }, - { - 'id': '3468206646', - 'name': 'exactNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match':'exact', 'value': 45.5}]]] - }, - { - 'id': '3468206647', - 'name': 'gtNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match':'gt', 'value': 70 }]]] - }, - { - 'id': '3468206644', - 'name': 'ltNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match':'lt', 'value': 1.0 }]]] - }, - { - 'id': '3468206643', - 'name': 'exactBoolean', - 'conditions': ['and', ['or', ['or', {'name': 'should_do_it', 'type': 'custom_attribute', 'match':'exact', 'value': true}]]] - } - ], - 'groups': [], - 'attributes': [ - { - 'key': 'house', - 'id': '594015' - }, - { - 'key': 'lasers', - 'id': '594016' - }, - { - 'key': 'should_do_it', - 'id': '594017' - }, - { - 'key': 'favorite_ice_cream', - 'id': '594018' - } - ], - 'botFiltering': false, - 'accountId': '4879520872', - 'events': [ - { - 'key': 'item_bought', - 'id': '594089', - 'experimentIds': [ - '11564051718', - '1323241597' - ] - }, - { - 'key': 'user_signed_up', - 'id': '594090', - 'experimentIds': ['1323241598', '1323241599'], - } - ], - 'revision': '3' -}; - -var getTypedAudiencesConfig = function() { - return cloneDeep(typedAudiencesConfig); -}; - -var typedAudiencesById = { - 3468206642: { - 'id': '3468206642', - 'name': 'exactString', - 'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute', 'value': 'Gryffindor'}]]] - }, - 3988293898: { - 'id': '3988293898', - 'name': 'substringString', - 'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute', 'match': 'substring', 'value': 'Slytherin'}]]], - }, - 3988293899: { - 'id': '3988293899', - 'name': 'exists', - 'conditions': ['and', ['or', ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute', 'match': 'exists'}]]], - }, - 3468206646: { - 'id': '3468206646', - 'name': 'exactNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'exact', 'value': 45.5}]]] - }, - 3468206647: { - 'id': '3468206647', - 'name': 'gtNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'gt', 'value': 70}]]] - }, - 3468206644: { - 'id': '3468206644', - 'name': 'ltNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'lt', 'value': 1.0}]]] - }, - 3468206643: { - 'id': '3468206643', - 'name': 'exactBoolean', - 'conditions': ['and', ['or', ['or', {'name': 'should_do_it', 'type': 'custom_attribute', 'match': 'exact', 'value': true}]]] - }, - 0: { - 'id': '0', - 'name': '$$dummy', - 'conditions': { 'type': 'custom_attribute', 'name': '$opt_dummy_attribute', 'value': 'impossible_value' }, - } -}; - -module.exports = { - getTestProjectConfig: getTestProjectConfig, - getParsedAudiences: getParsedAudiences, - getTestProjectConfigWithFeatures: getTestProjectConfigWithFeatures, - datafileWithFeaturesExpectedData: datafileWithFeaturesExpectedData, - getUnsupportedVersionConfig: getUnsupportedVersionConfig, - getTypedAudiencesConfig: getTypedAudiencesConfig, - typedAudiencesById: typedAudiencesById, -}; diff --git a/packages/optimizely-sdk/lib/utils/attributes_validator/index.js b/packages/optimizely-sdk/lib/utils/attributes_validator/index.js deleted file mode 100644 index 1304740b5..000000000 --- a/packages/optimizely-sdk/lib/utils/attributes_validator/index.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright 2016, 2018-2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Provides utility method for validating that the attributes user has provided are valid - */ - -var sprintf = require('sprintf-js').sprintf; -var lodashForOwn = require('lodash/forOwn'); -var fns = require('../../utils/fns'); - -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; -var MODULE_NAME = 'ATTRIBUTES_VALIDATOR'; - -module.exports = { - /** - * Validates user's provided attributes - * @param {Object} attributes - * @return {boolean} True if the attributes are valid - * @throws If the attributes are not valid - */ - validate: function(attributes) { - if (typeof attributes === 'object' && !Array.isArray(attributes) && attributes !== null) { - lodashForOwn(attributes, function(value, key) { - if (typeof value === 'undefined') { - throw new Error(sprintf(ERROR_MESSAGES.UNDEFINED_ATTRIBUTE, MODULE_NAME, key)); - } - }); - return true; - } else { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, MODULE_NAME)); - } - }, - - isAttributeValid: function(attributeKey, attributeValue) { - return (typeof attributeKey === 'string') && - (typeof attributeValue === 'string' || typeof attributeValue === 'boolean' || (fns.isNumber(attributeValue) && fns.isFinite(attributeValue))); - }, -}; diff --git a/packages/optimizely-sdk/lib/utils/config_validator/index.js b/packages/optimizely-sdk/lib/utils/config_validator/index.js deleted file mode 100644 index 8606782f5..000000000 --- a/packages/optimizely-sdk/lib/utils/config_validator/index.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright 2016, 2018, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var sprintf = require('sprintf-js').sprintf; - -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; -var MODULE_NAME = 'CONFIG_VALIDATOR'; -var DATAFILE_VERSIONS = require('../enums').DATAFILE_VERSIONS; - -var SUPPORTED_VERSIONS = [ - DATAFILE_VERSIONS.V2, - DATAFILE_VERSIONS.V3, - DATAFILE_VERSIONS.V4 -]; - -/** - * Provides utility methods for validating that the configuration options are valid - */ -module.exports = { - /** - * Validates the given config options - * @param {Object} config - * @param {Object} config.errorHandler - * @param {Object} config.eventDispatcher - * @param {Object} config.logger - * @return {Boolean} True if the config options are valid - * @throws If any of the config options are not valid - */ - validate: function(config) { - if (config.errorHandler && (typeof config.errorHandler.handleError !== 'function')) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_ERROR_HANDLER, MODULE_NAME)); - } - - if (config.eventDispatcher && (typeof config.eventDispatcher.dispatchEvent !== 'function')) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_EVENT_DISPATCHER, MODULE_NAME)); - } - - if (config.logger && (typeof config.logger.log !== 'function')) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_LOGGER, MODULE_NAME)); - } - - return true; - }, - - /** - * Validates the datafile - * @param {string} datafile - * @return {Boolean} True if the datafile is valid - * @throws If the datafile is not valid for any of the following reasons: - - The datafile string is undefined - - The datafile string cannot be parsed as a JSON object - - The datafile version is not supported - */ - validateDatafile: function(datafile) { - if (!datafile) { - throw new Error(sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, MODULE_NAME)); - } - - if (typeof datafile === 'string' || datafile instanceof String) { - // Attempt to parse the datafile string - try { - datafile = JSON.parse(datafile); - } catch (ex) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_DATAFILE_MALFORMED, MODULE_NAME)); - } - } - - if (SUPPORTED_VERSIONS.indexOf(datafile.version) === -1) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_DATAFILE_VERSION, MODULE_NAME, datafile.version)); - } - - return true; - } -}; diff --git a/packages/optimizely-sdk/lib/utils/enums/index.js b/packages/optimizely-sdk/lib/utils/enums/index.js deleted file mode 100644 index 91bc01871..000000000 --- a/packages/optimizely-sdk/lib/utils/enums/index.js +++ /dev/null @@ -1,210 +0,0 @@ -/**************************************************************************** - * Copyright 2016-2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -/** - * Contains global enums used throughout the library - */ -exports.LOG_LEVEL = { - NOTSET: 0, - DEBUG: 1, - INFO: 2, - WARNING: 3, - ERROR: 4, -}; - -exports.ERROR_MESSAGES = { - EXPERIMENT_KEY_NOT_IN_DATAFILE: '%s: Experiment key %s is not in datafile.', - FEATURE_NOT_IN_DATAFILE: '%s: Feature key %s is not in datafile.', - IMPROPERLY_FORMATTED_EXPERIMENT: '%s: Experiment key %s is improperly formatted.', - INVALID_ATTRIBUTES: '%s: Provided attributes are in an invalid format.', - INVALID_BUCKETING_ID: '%s: Unable to generate hash for bucketing ID %s: %s', - INVALID_DATAFILE: '%s: Datafile is invalid - property %s: %s', - INVALID_DATAFILE_MALFORMED: '%s: Datafile is invalid because it is malformed.', - INVALID_JSON: '%s: JSON object is not valid.', - INVALID_ERROR_HANDLER: '%s: Provided "errorHandler" is in an invalid format.', - INVALID_EVENT_DISPATCHER: '%s: Provided "eventDispatcher" is in an invalid format.', - INVALID_EVENT_KEY: '%s: Event key %s is not in datafile.', - INVALID_EVENT_TAGS: '%s: Provided event tags are in an invalid format.', - INVALID_EXPERIMENT_KEY: '%s: Experiment key %s is not in datafile. It is either invalid, paused, or archived.', - INVALID_EXPERIMENT_ID: '%s: Experiment ID %s is not in datafile.', - INVALID_GROUP_ID: '%s: Group ID %s is not in datafile.', - INVALID_LOGGER: '%s: Provided "logger" is in an invalid format.', - INVALID_ROLLOUT_ID: '%s: Invalid rollout ID %s attached to feature %s', - INVALID_USER_ID: '%s: Provided user ID is in an invalid format.', - INVALID_USER_PROFILE_SERVICE: '%s: Provided user profile service instance is in an invalid format: %s.', - JSON_SCHEMA_EXPECTED: '%s: JSON schema expected.', - NO_DATAFILE_SPECIFIED: '%s: No datafile specified. Cannot start optimizely.', - NO_JSON_PROVIDED: '%s: No JSON object to validate against schema.', - NO_VARIATION_FOR_EXPERIMENT_KEY: '%s: No variation key %s defined in datafile for experiment %s.', - UNDEFINED_ATTRIBUTE: '%s: Provided attribute: %s has an undefined value.', - UNRECOGNIZED_ATTRIBUTE: '%s: Unrecognized attribute %s provided. Pruning before sending event to Optimizely.', - UNABLE_TO_CAST_VALUE: '%s: Unable to cast value %s to type %s, returning null.', - USER_NOT_IN_FORCED_VARIATION: '%s: User %s is not in the forced variation map. Cannot remove their forced variation.', - USER_PROFILE_LOOKUP_ERROR: '%s: Error while looking up user profile for user ID "%s": %s.', - USER_PROFILE_SAVE_ERROR: '%s: Error while saving user profile for user ID "%s": %s.', - VARIABLE_KEY_NOT_IN_DATAFILE: '%s: Variable with key "%s" associated with feature with key "%s" is not in datafile.', - VARIATION_ID_NOT_IN_DATAFILE: '%s: No variation ID %s defined in datafile for experiment %s.', - VARIATION_ID_NOT_IN_DATAFILE_NO_EXPERIMENT: '%s: Variation ID %s is not in the datafile.', - INVALID_INPUT_FORMAT: '%s: Provided %s is in an invalid format.', - INVALID_DATAFILE_VERSION: '%s: This version of the JavaScript SDK does not support the given datafile version: %s', - INVALID_VARIATION_KEY: '%s: Provided variation key is in an invalid format.', -}; - -exports.LOG_MESSAGES = { - ACTIVATE_USER: '%s: Activating user %s in experiment %s.', - DISPATCH_CONVERSION_EVENT: '%s: Dispatching conversion event to URL %s with params %s.', - DISPATCH_IMPRESSION_EVENT: '%s: Dispatching impression event to URL %s with params %s.', - DEPRECATED_EVENT_VALUE: '%s: Event value is deprecated in %s call.', - EXPERIMENT_NOT_RUNNING: '%s: Experiment %s is not running.', - FEATURE_ENABLED_FOR_USER: '%s: Feature %s is enabled for user %s.', - FEATURE_NOT_ENABLED_FOR_USER: '%s: Feature %s is not enabled for user %s.', - FEATURE_HAS_NO_EXPERIMENTS: '%s: Feature %s is not attached to any experiments.', - FAILED_TO_PARSE_VALUE: '%s: Failed to parse event value "%s" from event tags.', - FAILED_TO_PARSE_REVENUE: '%s: Failed to parse revenue value "%s" from event tags.', - FORCED_BUCKETING_FAILED: '%s: Variation key %s is not in datafile. Not activating user %s.', - INVALID_OBJECT: '%s: Optimizely object is not valid. Failing %s.', - INVALID_CLIENT_ENGINE: '%s: Invalid client engine passed: %s. Defaulting to node-sdk.', - INVALID_VARIATION_ID: '%s: Bucketed into an invalid variation ID. Returning null.', - NOTIFICATION_LISTENER_EXCEPTION: '%s: Notification listener for (%s) threw exception: %s', - NO_ROLLOUT_EXISTS: '%s: There is no rollout of feature %s.', - NOT_ACTIVATING_USER: '%s: Not activating user %s for experiment %s.', - NOT_TRACKING_USER: '%s: Not tracking user %s.', - PARSED_REVENUE_VALUE: '%s: Parsed revenue value "%s" from event tags.', - PARSED_NUMERIC_VALUE: '%s: Parsed event value "%s" from event tags.', - RETURNING_STORED_VARIATION: '%s: Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile.', - ROLLOUT_HAS_NO_EXPERIMENTS: '%s: Rollout of feature %s has no experiments', - SAVED_VARIATION: '%s: Saved variation "%s" of experiment "%s" for user "%s".', - SAVED_VARIATION_NOT_FOUND: '%s: User %s was previously bucketed into variation with ID %s for experiment %s, but no matching variation was found.', - SHOULD_NOT_DISPATCH_ACTIVATE: '%s: Experiment %s is in "Launched" state. Not activating user.', - SKIPPING_JSON_VALIDATION: '%s: Skipping JSON schema validation.', - TRACK_EVENT: '%s: Tracking event %s for user %s.', - USER_ASSIGNED_TO_VARIATION_BUCKET: '%s: Assigned variation bucket %s to user %s.', - USER_ASSIGNED_TO_EXPERIMENT_BUCKET: '%s: Assigned experiment bucket %s to user %s.', - USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP: '%s: User %s is in experiment %s of group %s.', - USER_BUCKETED_INTO_TARGETING_RULE: '%s: User %s bucketed into targeting rule %s.', - USER_IN_FEATURE_EXPERIMENT: '%s: User %s is in variation %s of experiment %s on the feature %s.', - USER_IN_ROLLOUT: '%s: User %s is in rollout of feature %s.', - USER_BUCKETED_INTO_EVERYONE_TARGETING_RULE: '%s: User %s bucketed into everyone targeting rule.', - USER_NOT_BUCKETED_INTO_EVERYONE_TARGETING_RULE: '%s: User %s not bucketed into everyone targeting rule due to traffic allocation.', - USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP: '%s: User %s is not in experiment %s of group %s.', - USER_NOT_BUCKETED_INTO_ANY_EXPERIMENT_IN_GROUP: '%s: User %s is not in any experiment of group %s.', - USER_NOT_BUCKETED_INTO_TARGETING_RULE: '%s User %s not bucketed into targeting rule %s due to traffic allocation. Trying everyone rule.', - USER_NOT_IN_FEATURE_EXPERIMENT: '%s: User %s is not in any experiment on the feature %s.', - USER_NOT_IN_ROLLOUT: '%s: User %s is not in rollout of feature %s.', - USER_FORCED_IN_VARIATION: '%s: User %s is forced in variation %s.', - USER_MAPPED_TO_FORCED_VARIATION: '%s: Set variation %s for experiment %s and user %s in the forced variation map.', - USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE: '%s: User %s does not meet conditions for targeting rule %s.', - USER_MEETS_CONDITIONS_FOR_TARGETING_RULE: '%s: User %s meets conditions for targeting rule %s.', - USER_HAS_VARIATION: '%s: User %s is in variation %s of experiment %s.', - USER_HAS_FORCED_VARIATION: '%s: Variation %s is mapped to experiment %s and user %s in the forced variation map.', - USER_HAS_NO_VARIATION: '%s: User %s is in no variation of experiment %s.', - USER_HAS_NO_FORCED_VARIATION: '%s: User %s is not in the forced variation map.', - USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT: '%s: No experiment %s mapped to user %s in the forced variation map.', - USER_NOT_IN_ANY_EXPERIMENT: '%s: User %s is not in any experiment of group %s.', - USER_NOT_IN_EXPERIMENT: '%s: User %s does not meet conditions to be in experiment %s.', - USER_RECEIVED_DEFAULT_VARIABLE_VALUE: '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".', - USER_RECEIVED_VARIABLE_VALUE: '%s: Value for variable "%s" of feature flag "%s" is %s for user "%s"', - VALID_DATAFILE: '%s: Datafile is valid.', - VALID_USER_PROFILE_SERVICE: '%s: Valid user profile service provided.', - VARIATION_REMOVED_FOR_USER: '%s: Variation mapped to experiment %s has been removed for user %s.', - VARIABLE_REQUESTED_WITH_WRONG_TYPE: '%s: Requested variable type "%s", but variable is of type "%s". Use correct API to retrieve value. Returning None.', - VALID_BUCKETING_ID: '%s: BucketingId is valid: "%s"', - BUCKETING_ID_NOT_STRING: '%s: BucketingID attribute is not a string. Defaulted to userId', - EVALUATING_AUDIENCE: '%s: Starting to evaluate audience "%s" with conditions: %s.', - EVALUATING_AUDIENCES_COMBINED: '%s: Evaluating audiences for experiment "%s": %s.', - AUDIENCE_EVALUATION_RESULT: '%s: Audience "%s" evaluated to %s.', - AUDIENCE_EVALUATION_RESULT_COMBINED: '%s: Audiences for experiment %s collectively evaluated to %s.', - MISSING_ATTRIBUTE_VALUE: '%s: Audience condition %s evaluated to UNKNOWN because no value was passed for user attribute "%s".', - UNEXPECTED_CONDITION_VALUE: '%s: Audience condition %s evaluated to UNKNOWN because the condition value is not supported.', - UNEXPECTED_TYPE: '%s: Audience condition %s evaluated to UNKNOWN because a value of type "%s" was passed for user attribute "%s".', - UNEXPECTED_TYPE_NULL: '%s: Audience condition %s evaluated to UNKNOWN because a null value was passed for user attribute "%s".', - UNKNOWN_CONDITION_TYPE: '%s: Audience condition %s has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.', - UNKNOWN_MATCH_TYPE: '%s: Audience condition %s uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK.', - OUT_OF_BOUNDS: '%s: Audience condition %s evaluated to UNKNOWN because the number value for user attribute "%s" is not in the range [-2^53, +2^53].', -}; - -exports.RESERVED_EVENT_KEYWORDS = { - REVENUE: 'revenue', - VALUE: 'value', -}; - -exports.CONTROL_ATTRIBUTES = { - BOT_FILTERING: '$opt_bot_filtering', - BUCKETING_ID: '$opt_bucketing_id', - STICKY_BUCKETING_KEY: '$opt_experiment_bucket_map', - USER_AGENT: '$opt_user_agent', -}; - -exports.JAVASCRIPT_CLIENT_ENGINE = 'javascript-sdk'; -exports.NODE_CLIENT_ENGINE = 'node-sdk'; -exports.NODE_CLIENT_VERSION = '3.1.0-beta1'; - -/* - * Notification types for use with NotificationCenter - * Format is EVENT: <list of parameters to callback> - * - * SDK consumers can use these to register callbacks with the notification center. - * - * ACTIVATE: An impression event will be sent to Optimizely - * Callbacks will receive an object argument with the following properties: - * - experiment {Object} - * - userId {string} - * - attributes {Object|undefined} - * - variation {Object} - * - logEvent {Object} - * - * TRACK: A conversion event will be sent to Optimizely - * Callbacks will receive the an object argument with the following properties: - * - eventKey {string} - * - userId {string} - * - attributes {Object|undefined} - * - eventTags {Object|undefined} - * - logEvent {Object} - */ -exports.NOTIFICATION_TYPES = { - ACTIVATE: 'ACTIVATE:experiment, user_id,attributes, variation, event', - TRACK: 'TRACK:event_key, user_id, attributes, event_tags, event', -}; - -/* - * Represents the source of a decision for feature management. When a feature - * is accessed through isFeatureEnabled or getVariableValue APIs, the decision - * source is used to decide whether to dispatch an impression event to - * Optimizely. - */ -exports.DECISION_SOURCES = { - EXPERIMENT: 'experiment', - ROLLOUT: 'rollout', -}; - -/* - * Possible types of variables attached to features - */ -exports.FEATURE_VARIABLE_TYPES = { - BOOLEAN: 'boolean', - DOUBLE: 'double', - INTEGER: 'integer', - STRING: 'string', -}; - -/* - * Supported datafile versions - */ -exports.DATAFILE_VERSIONS = { - V2: '2', - V3: '3', - V4: '4', -}; diff --git a/packages/optimizely-sdk/lib/utils/event_tag_utils/index.js b/packages/optimizely-sdk/lib/utils/event_tag_utils/index.js deleted file mode 100644 index ac3745735..000000000 --- a/packages/optimizely-sdk/lib/utils/event_tag_utils/index.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright 2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Provides utility method for parsing event tag values - */ -var enums = require('../enums'); -var sprintf = require('sprintf-js').sprintf; - -var LOG_LEVEL = enums.LOG_LEVEL; -var LOG_MESSAGES = enums.LOG_MESSAGES; -var MODULE_NAME = 'EVENT_TAG_UTILS'; -var REVENUE_EVENT_METRIC_NAME = enums.RESERVED_EVENT_KEYWORDS.REVENUE; -var VALUE_EVENT_METRIC_NAME = enums.RESERVED_EVENT_KEYWORDS.VALUE; - -module.exports = { - /** - * Grab the revenue value from the event tags. "revenue" is a reserved keyword. - * @param {Object} eventTags - * @param {Object} logger - * @return {Integer|null} - */ - getRevenueValue: function(eventTags, logger) { - if (eventTags && eventTags.hasOwnProperty(REVENUE_EVENT_METRIC_NAME)) { - var rawValue = eventTags[REVENUE_EVENT_METRIC_NAME]; - var parsedRevenueValue = parseInt(rawValue, 10); - if (isNaN(parsedRevenueValue)) { - logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.FAILED_TO_PARSE_REVENUE, MODULE_NAME, rawValue)); - return null; - } - logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.PARSED_REVENUE_VALUE, MODULE_NAME, parsedRevenueValue)); - return parsedRevenueValue; - } - return null; - }, - - /** - * Grab the event value from the event tags. "value" is a reserved keyword. - * @param {Object} eventTags - * @param {Object} logger - * @return {Number|null} - */ - getEventValue: function(eventTags, logger) { - if (eventTags && eventTags.hasOwnProperty(VALUE_EVENT_METRIC_NAME)) { - var rawValue = eventTags[VALUE_EVENT_METRIC_NAME]; - var parsedEventValue = parseFloat(rawValue); - if (isNaN(parsedEventValue)) { - logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.FAILED_TO_PARSE_VALUE, MODULE_NAME, rawValue)); - return null; - } - logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.PARSED_NUMERIC_VALUE, MODULE_NAME, parsedEventValue)); - return parsedEventValue; - } - return null; - }, -}; diff --git a/packages/optimizely-sdk/lib/utils/fns/index.js b/packages/optimizely-sdk/lib/utils/fns/index.js deleted file mode 100644 index b9b3bf3ec..000000000 --- a/packages/optimizely-sdk/lib/utils/fns/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2017, 2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var uuid = require('uuid'); -var _isFinite = require('lodash/isFinite'); -var MAX_NUMBER_LIMIT = Math.pow(2, 53); - -module.exports = { - assign: require('lodash/assign'), - assignIn: require('lodash/assignIn'), - cloneDeep: require('lodash/cloneDeep'), - currentTimestamp: function() { - return Math.round(new Date().getTime()); - }, - isArray: require('lodash/isArray'), - isEmpty: require('lodash/isEmpty'), - isFinite: function(number) { - return _isFinite(number) && Math.abs(number) <= MAX_NUMBER_LIMIT; - }, - keyBy: require('lodash/keyBy'), - filter: require('lodash/filter'), - forEach: require('lodash/forEach'), - forOwn: require('lodash/forOwn'), - map: require('lodash/map'), - uuid: function() { - return uuid.v4(); - }, - values: require('lodash/values'), - isNumber: require('lodash/isNumber'), -}; diff --git a/packages/optimizely-sdk/lib/utils/fns/index.tests.js b/packages/optimizely-sdk/lib/utils/fns/index.tests.js deleted file mode 100644 index fba8890b8..000000000 --- a/packages/optimizely-sdk/lib/utils/fns/index.tests.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright 2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var chai = require('chai'); -var assert = chai.assert; -var fns = require('./'); - -describe('lib/utils/fns', function() { - describe('APIs', function() { - describe('isFinite', function() { - it('should return false for invalid numbers', function() { - assert.isFalse(fns.isFinite(Infinity)); - assert.isFalse(fns.isFinite(-Infinity)); - assert.isFalse(fns.isFinite(NaN)); - assert.isFalse(fns.isFinite(Math.pow(2, 53) + 2)); - assert.isFalse(fns.isFinite(-Math.pow(2, 53) - 2)); - }); - - it('should return true for valid numbers', function() { - assert.isTrue(fns.isFinite(0)); - assert.isTrue(fns.isFinite(10)); - assert.isTrue(fns.isFinite(10.5)); - assert.isTrue(fns.isFinite(Math.pow(2, 53))); - assert.isTrue(fns.isFinite(-Math.pow(2, 53))); - }); - }); - }); -}); \ No newline at end of file diff --git a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.js b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.js deleted file mode 100644 index aef06c827..000000000 --- a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var fns = require('../fns'); -var validate = require('json-schema').validate; -var sprintf = require('sprintf-js').sprintf; - -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; -var MODULE_NAME = 'JSON_SCHEMA_VALIDATOR'; - -module.exports = { - /** - * Validate the given json object against the specified schema - * @param {Object} jsonSchema The json schema to validate against - * @param {Object} jsonObject The object to validate against the schema - * @return {Boolean} True if the given object is valid - */ - validate: function(jsonSchema, jsonObject) { - if (!jsonSchema) { - throw new Error(sprintf(ERROR_MESSAGES.JSON_SCHEMA_EXPECTED, MODULE_NAME)); - } - - if (!jsonObject) { - throw new Error(sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, MODULE_NAME)); - } - - var result = validate(jsonObject, jsonSchema); - if (result.valid) { - return true; - } else { - if (fns.isArray(result.errors)) { - throw new Error(sprintf(ERROR_MESSAGES.INVALID_DATAFILE, MODULE_NAME, result.errors[0].property, result.errors[0].message)); - } - throw new Error(sprintf(ERROR_MESSAGES.INVALID_JSON, MODULE_NAME)); - } - } -}; diff --git a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js b/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js deleted file mode 100644 index f6389243f..000000000 --- a/packages/optimizely-sdk/lib/utils/json_schema_validator/index.tests.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright 2016-2017, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -var chai = require('chai'); -var assert = chai.assert; -var jsonSchemaValidator = require('./'); -var projectConfigSchema = require('../../optimizely/project_config_schema'); -var sprintf = require('sprintf-js').sprintf; -var testData = require('../../tests/test_data.js'); - -var ERROR_MESSAGES = require('../enums').ERROR_MESSAGES; - -describe('lib/utils/json_schema_validator', function() { - describe('APIs', function() { - describe('validate', function() { - it('should validate the given object against the specified schema', function() { - assert.isTrue(jsonSchemaValidator.validate({'type': 'number'}, 4)); - }); - - it('should throw an error if the object is not valid', function() { - assert.throws(function() { - jsonSchemaValidator.validate({'type': 'number'}, 'not a number'); - }, sprintf(ERROR_MESSAGES.INVALID_DATAFILE, 'JSON_SCHEMA_VALIDATOR', '', 'string value found, but a number is required')); - }); - - it('should throw an error if no schema is passed in', function() { - assert.throws(function() { - jsonSchemaValidator.validate(); - }, sprintf(ERROR_MESSAGES.JSON_SCHEMA_EXPECTED, 'JSON_SCHEMA_VALIDATOR')); - }); - - it('should throw an error if no json object is passed in', function() { - assert.throws(function() { - jsonSchemaValidator.validate({'type': 'number'}); - }, sprintf(ERROR_MESSAGES.NO_JSON_PROVIDED, 'JSON_SCHEMA_VALIDATOR')); - }); - - it('should validate specified Optimizely datafile with the Optimizely datafile schema', function() { - assert.isTrue(jsonSchemaValidator.validate(projectConfigSchema, testData.getTestProjectConfig())); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json deleted file mode 100644 index 27e54de86..000000000 --- a/packages/optimizely-sdk/package-lock.json +++ /dev/null @@ -1,8105 +0,0 @@ -{ - "name": "@optimizely/optimizely-sdk", - "version": "3.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@optimizely/js-sdk-logging": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.1.0.tgz", - "integrity": "sha512-Bs2zHvsdNIk2QSg05P6mKIlROHoBIRNStbrVwlePm603CucojKRPlFJG4rt7sFZQOo8xS8I7z1BmE4QI3/ZE9A==", - "requires": { - "@optimizely/js-sdk-utils": "^0.1.0" - } - }, - "@optimizely/js-sdk-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.1.0.tgz", - "integrity": "sha512-p7499GgVaX94YmkrwOiEtLgxgjXTPbUQsvETaAil5J7zg1TOA4Wl8ClalLSvCh+AKWkxGdkL4/uM/zfbxPSNNw==", - "requires": { - "uuid": "^3.3.2" - } - }, - "@webassemblyjs/ast": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", - "integrity": "sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/wast-parser": "1.7.11" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz", - "integrity": "sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz", - "integrity": "sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz", - "integrity": "sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz", - "integrity": "sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.7.11" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz", - "integrity": "sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz", - "integrity": "sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==", - "dev": true - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz", - "integrity": "sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz", - "integrity": "sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-buffer": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/wasm-gen": "1.7.11" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz", - "integrity": "sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.11.tgz", - "integrity": "sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.1" - } - }, - "@webassemblyjs/utf8": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.11.tgz", - "integrity": "sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz", - "integrity": "sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-buffer": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/helper-wasm-section": "1.7.11", - "@webassemblyjs/wasm-gen": "1.7.11", - "@webassemblyjs/wasm-opt": "1.7.11", - "@webassemblyjs/wasm-parser": "1.7.11", - "@webassemblyjs/wast-printer": "1.7.11" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz", - "integrity": "sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/ieee754": "1.7.11", - "@webassemblyjs/leb128": "1.7.11", - "@webassemblyjs/utf8": "1.7.11" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz", - "integrity": "sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-buffer": "1.7.11", - "@webassemblyjs/wasm-gen": "1.7.11", - "@webassemblyjs/wasm-parser": "1.7.11" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz", - "integrity": "sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-api-error": "1.7.11", - "@webassemblyjs/helper-wasm-bytecode": "1.7.11", - "@webassemblyjs/ieee754": "1.7.11", - "@webassemblyjs/leb128": "1.7.11", - "@webassemblyjs/utf8": "1.7.11" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz", - "integrity": "sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/floating-point-hex-parser": "1.7.11", - "@webassemblyjs/helper-api-error": "1.7.11", - "@webassemblyjs/helper-code-frame": "1.7.11", - "@webassemblyjs/helper-fsm": "1.7.11", - "@xtuc/long": "4.2.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz", - "integrity": "sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/wast-parser": "1.7.11", - "@xtuc/long": "4.2.1" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz", - "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "dev": true, - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", - "dev": true - }, - "acorn-dynamic-import": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", - "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", - "dev": true, - "requires": { - "acorn": "^5.0.0" - } - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "dev": true, - "requires": { - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", - "dev": true - }, - "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "dev": true - }, - "blob": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", - "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", - "dev": true - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "dev": true, - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserstack": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.0.tgz", - "integrity": "sha1-tWVCWtYu1ywQgqHrl51TE8fUdU8=", - "dev": true, - "requires": { - "https-proxy-agent": "1.0.0" - }, - "dependencies": { - "agent-base": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", - "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", - "dev": true, - "requires": { - "extend": "~3.0.0", - "semver": "~5.0.1" - } - }, - "https-proxy-agent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", - "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", - "dev": true, - "requires": { - "agent-base": "2", - "debug": "2", - "extend": "3" - } - }, - "semver": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", - "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", - "dev": true - } - } - }, - "browserstacktunnel-wrapper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/browserstacktunnel-wrapper/-/browserstacktunnel-wrapper-2.0.2.tgz", - "integrity": "sha512-7w7HYA00qjBtuQH0c5rqW7RbWPHyRROqTZofwNp5G0sKc2fYChsTfbHz3ul8Yd+ffkQvR81m+iPjEB004P6kxQ==", - "dev": true, - "requires": { - "https-proxy-agent": "^1.0.0", - "unzip": "~0.1.9" - }, - "dependencies": { - "agent-base": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", - "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", - "dev": true, - "requires": { - "extend": "~3.0.0", - "semver": "~5.0.1" - } - }, - "https-proxy-agent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", - "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", - "dev": true, - "requires": { - "agent-base": "2", - "debug": "2", - "extend": "3" - } - }, - "semver": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", - "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", - "dev": true - } - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "cacache": { - "version": "10.0.4", - "resolved": "http://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", - "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", - "dev": true, - "requires": { - "bluebird": "^3.5.1", - "chownr": "^1.0.1", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "lru-cache": "^4.1.1", - "mississippi": "^2.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^5.2.4", - "unique-filename": "^1.1.0", - "y18n": "^4.0.0" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha1-dgqnLPION5XoSxKHfODoNzeqKeU=", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", - "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "combine-lists": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", - "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", - "dev": true, - "requires": { - "lodash": "^4.5.0" - } - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", - "dev": true, - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", - "utils-merge": "1.0.1" - } - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.6.tgz", - "integrity": "sha512-lQUVfQi0aLix2xpyjrrJEvfuYCqPc/HwmTKsC/VNf8q0zsjX7SQZtp4+oRONN5Tsur9GDETPjj+Ub2iDiGZfSQ==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "coveralls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.2.tgz", - "integrity": "sha1-9aC82Qyk5k4Ii3EPqN2mQK6kiE8=", - "dev": true, - "requires": { - "growl": "~> 1.10.0", - "js-yaml": "^3.11.0", - "lcov-parse": "^0.0.10", - "log-driver": "^1.2.7", - "minimist": "^1.2.0", - "request": "^2.85.0" - }, - "dependencies": { - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "dev": true, - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - } - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha1-6u1lbsg0TxD1J8a/obbiJE3hZ9E=", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", - "dev": true - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "dev": true, - "requires": { - "mime-db": "~1.37.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", - "dev": true - }, - "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", - "dev": true - }, - "d": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date-format": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz", - "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=", - "dev": true - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "optional": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "http://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", - "dev": true, - "requires": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "duplexify": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", - "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "dev": true, - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "engine.io-parser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", - "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", - "dev": true, - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.4", - "has-binary2": "~1.0.2" - } - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", - "dev": true - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "es5-ext": { - "version": "0.10.46", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", - "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, - "dependencies": { - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - } - } - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "^0.1.3", - "es6-weak-map": "^2.0.1", - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint": { - "version": "2.13.1", - "resolved": "http://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", - "integrity": "sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "concat-stream": "^1.4.6", - "debug": "^2.1.1", - "doctrine": "^1.2.2", - "es6-map": "^0.1.3", - "escope": "^3.6.0", - "espree": "^3.1.6", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^1.1.1", - "glob": "^7.0.3", - "globals": "^9.2.0", - "ignore": "^3.1.2", - "imurmurhash": "^0.1.4", - "inquirer": "^0.12.0", - "is-my-json-valid": "^2.10.0", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.5.1", - "json-stable-stringify": "^1.0.0", - "levn": "^0.3.0", - "lodash": "^4.0.0", - "mkdirp": "^0.5.0", - "optionator": "^0.8.1", - "path-is-absolute": "^1.0.0", - "path-is-inside": "^1.0.1", - "pluralize": "^1.2.1", - "progress": "^1.1.8", - "require-uncached": "^1.0.2", - "shelljs": "^0.6.0", - "strip-json-comments": "~1.0.1", - "table": "^3.7.8", - "text-table": "~0.2.0", - "user-home": "^2.0.0" - } - }, - "espree": { - "version": "3.5.4", - "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "dev": true, - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", - "dev": true - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-braces": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", - "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", - "dev": true, - "requires": { - "array-slice": "^0.2.3", - "array-unique": "^0.2.1", - "braces": "^0.1.2" - }, - "dependencies": { - "braces": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", - "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", - "dev": true, - "requires": { - "expand-range": "^0.1.0" - } - }, - "expand-range": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", - "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", - "dev": true, - "requires": { - "is-number": "^0.1.1", - "repeat-string": "^0.2.2" - } - }, - "is-number": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", - "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", - "dev": true - }, - "repeat-string": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", - "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", - "dev": true - } - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz", - "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.1", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", - "dev": true - } - } - }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - } - }, - "flatted": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", - "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", - "dev": true - }, - "flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" - } - }, - "follow-redirects": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz", - "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==", - "dev": true, - "requires": { - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - } - }, - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.x" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-access": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", - "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", - "dev": true, - "requires": { - "null-check": "^1.0.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minipass": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", - "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", - "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", - "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", - "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", - "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", - "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", - "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true - } - } - }, - "fstream": { - "version": "0.1.31", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-0.1.31.tgz", - "integrity": "sha1-czfwWPu7vvqMn1YaKMqwhJICyYg=", - "dev": true, - "requires": { - "graceful-fs": "~3.0.2", - "inherits": "~2.0.0", - "mkdirp": "0.5", - "rimraf": "2" - }, - "dependencies": { - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "dev": true, - "requires": { - "natives": "^1.1.0" - } - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "global-modules-path": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.0.tgz", - "integrity": "sha512-HchvMJNYh9dGSCy8pOQ2O8u/hoXaL+0XhnrwH0RyLiSXMMTl9W3N6KUU73+JFOg5PGjtzl6VZzUQsnrpm7Szag==", - "dev": true - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", - "dev": true - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "dev": true, - "requires": { - "eventemitter3": "^3.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "inquirer": { - "version": "0.12.0", - "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", - "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "ansi-regex": "^2.0.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "readline2": "^1.0.1", - "run-async": "^0.1.0", - "rx-lite": "^3.1.2", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isbinaryfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", - "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "js-yaml": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha1-3KFKcCNf+C8KyaOr62DTN6NlGF0=", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "dev": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "karma": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/karma/-/karma-3.1.4.tgz", - "integrity": "sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw==", - "dev": true, - "requires": { - "bluebird": "^3.3.0", - "body-parser": "^1.16.1", - "chokidar": "^2.0.3", - "colors": "^1.1.0", - "combine-lists": "^1.0.0", - "connect": "^3.6.0", - "core-js": "^2.2.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.0", - "expand-braces": "^0.1.1", - "flatted": "^2.0.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "http-proxy": "^1.13.0", - "isbinaryfile": "^3.0.0", - "lodash": "^4.17.5", - "log4js": "^3.0.0", - "mime": "^2.3.1", - "minimatch": "^3.0.2", - "optimist": "^0.6.1", - "qjobs": "^1.1.4", - "range-parser": "^1.2.0", - "rimraf": "^2.6.0", - "safe-buffer": "^5.0.1", - "socket.io": "2.1.1", - "source-map": "^0.6.1", - "tmp": "0.0.33", - "useragent": "2.3.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "circular-json": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz", - "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==", - "dev": true - }, - "engine.io": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "engine.io-client": { - "version": "3.2.1", - "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "log4js": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.6.tgz", - "integrity": "sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ==", - "dev": true, - "requires": { - "circular-json": "^0.5.5", - "date-format": "^1.2.0", - "debug": "^3.1.0", - "rfdc": "^1.1.2", - "streamroller": "0.7.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", - "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "socket.io": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", - "dev": true, - "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.1", - "socket.io-parser": "~3.2.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", - "dev": true, - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", - "to-array": "0.1.4" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "socket.io-parser": { - "version": "3.2.0", - "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "useragent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", - "dev": true, - "requires": { - "lru-cache": "4.1.x", - "tmp": "0.0.x" - } - } - } - }, - "karma-browserstack-launcher": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.3.0.tgz", - "integrity": "sha1-Yf49NrHPEGgeQPnYdL83Jx+xxnQ=", - "dev": true, - "requires": { - "browserstack": "1.5.0", - "browserstacktunnel-wrapper": "~2.0.1", - "q": "~1.5.0" - } - }, - "karma-chai": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", - "integrity": "sha1-vuWtQEAFF4Ea40u5RfdikJEIt5o=", - "dev": true - }, - "karma-chrome-launcher": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", - "integrity": "sha1-zxudBxNswY/iOTJ9JGVMPbw2is8=", - "dev": true, - "requires": { - "fs-access": "^1.0.0", - "which": "^1.2.1" - } - }, - "karma-mocha": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-1.3.0.tgz", - "integrity": "sha1-7qrH/8DiAetjxGdEDStpx883eL8=", - "dev": true, - "requires": { - "minimist": "1.2.0" - } - }, - "karma-sinon": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz", - "integrity": "sha1-TjRD8oMP3s/2JNN0cWPxIX2qKpo=", - "dev": true - }, - "karma-webpack": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-3.0.5.tgz", - "integrity": "sha512-nRudGJWstvVuA6Tbju9tyGUfXTtI1UXMXoRHVmM2/78D0q6s/Ye2IC157PKNDC15PWFGR0mVIRtWLAdcfsRJoA==", - "dev": true, - "requires": { - "async": "^2.0.0", - "babel-runtime": "^6.0.0", - "loader-utils": "^1.0.0", - "lodash": "^4.0.0", - "source-map": "^0.5.6", - "webpack-dev-middleware": "^2.0.6" - }, - "dependencies": { - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "dev": true, - "requires": { - "lodash": "^4.17.10" - } - }, - "mime": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", - "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "webpack-dev-middleware": { - "version": "2.0.6", - "resolved": "http://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-2.0.6.tgz", - "integrity": "sha512-tj5LLD9r4tDuRIDa5Mu9lnY2qBBehAITv6A9irqXhw/HQquZgTx3BCd57zYbU2gMDnncA49ufK2qVQSbaKJwOw==", - "dev": true, - "requires": { - "loud-rejection": "^1.6.0", - "memory-fs": "~0.4.1", - "mime": "^2.1.0", - "path-is-absolute": "^1.0.0", - "range-parser": "^1.0.3", - "url-join": "^2.0.2", - "webpack-log": "^1.0.1" - } - } - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "loader-runner": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz", - "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "loglevelnext": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz", - "integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==", - "dev": true, - "requires": { - "es6-symbol": "^3.1.1", - "object.assign": "^4.1.0" - } - }, - "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "match-stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/match-stream/-/match-stream-0.0.2.tgz", - "integrity": "sha1-mesFAJOzTf+t5CG5rAtBCpz6F88=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "readable-stream": "~1.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "requires": { - "mime-db": "~1.33.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mississippi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", - "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^2.0.1", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha1-bYrlCPWRZ/lA8rWzxKYSrlDJCuY=", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "mocha-lcov-reporter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz", - "integrity": "sha1-Rpve9PivyaEWBW8HnfYYLQr7A4Q=", - "dev": true - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "murmurhash": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-0.0.2.tgz", - "integrity": "sha1-bwe9ihEF5wnCb8iUIMtZMMJFhf4=" - }, - "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", - "dev": true - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", - "dev": true - }, - "natives": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.3.tgz", - "integrity": "sha512-BZGSYV4YOLxzoTK73l0/s/0sH9l8SHs2ocReMH1f8JYSh5FUWu4ZrKCpJdRkWXV6HFR/pZDz7bwWOVAY07q77g==", - "dev": true - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true - }, - "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "nock": { - "version": "7.7.3", - "resolved": "http://registry.npmjs.org/nock/-/nock-7.7.3.tgz", - "integrity": "sha1-0GAJgKREPt9uULXtMxRgLLfMxIk=", - "dev": true, - "requires": { - "chai": ">=1.9.2 <4.0.0", - "debug": "^2.2.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^3.10.1", - "mkdirp": "^0.5.0", - "propagate": "0.3.x", - "qs": "^6.0.2" - }, - "dependencies": { - "chai": { - "version": "3.5.0", - "resolved": "http://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true, - "requires": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - } - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "http://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true - } - } - }, - "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^1.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.0", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.10.3", - "vm-browserify": "0.0.4" - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "null-check": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", - "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", - "dev": true, - "requires": { - "execa": "^0.10.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "over": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/over/-/over-0.0.5.tgz", - "integrity": "sha1-8phS5w/X4l82DgE6jsRMgq7bVwg=", - "dev": true - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", - "dev": true - }, - "parallel-transform": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", - "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", - "dev": true, - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", - "dev": true, - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" - } - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "pluralize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", - "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true - }, - "propagate": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.3.1.tgz", - "integrity": "sha1-46hEBKfs6CDda76p9tkk4xNa4Jw=", - "dev": true - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "pullstream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pullstream/-/pullstream-0.4.1.tgz", - "integrity": "sha1-1vs79a7Wl+gxFQ6xACwlo/iuExQ=", - "dev": true, - "requires": { - "over": ">= 0.0.5 < 1", - "readable-stream": "~1.0.31", - "setimmediate": ">= 1.0.2 < 2", - "slice-stream": ">= 1.0.0 < 2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", - "dev": true - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "dev": true, - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, - "readline2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", - "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "mute-stream": "0.0.5" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - } - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", - "dev": true - } - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rfdc": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz", - "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-async": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", - "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", - "dev": true, - "requires": { - "once": "^1.3.0" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rx-lite": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", - "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "dev": true - }, - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" - }, - "dependencies": { - "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - } - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "serialize-javascript": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz", - "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.6.1", - "resolved": "http://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", - "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "sinon": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", - "integrity": "sha1-Ah/WS1TLd9nS+w1Dze3652KcOjY=", - "dev": true, - "requires": { - "diff": "^3.1.0", - "formatio": "1.2.0", - "lolex": "^1.6.0", - "native-promise-only": "^0.8.1", - "path-to-regexp": "^1.7.0", - "samsam": "^1.1.3", - "text-encoding": "0.6.4", - "type-detect": "^4.0.0" - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", - "dev": true - }, - "slice-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-stream/-/slice-stream-1.0.0.tgz", - "integrity": "sha1-WzO9ZvATsaf4ZGCwPUY97DmtPqA=", - "dev": true, - "requires": { - "readable-stream": "~1.0.31" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", - "dev": true - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", - "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", - "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "streamroller": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz", - "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==", - "dev": true, - "requires": { - "date-format": "^1.2.0", - "debug": "^3.1.0", - "mkdirp": "^0.5.1", - "readable-stream": "^2.3.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "table": { - "version": "3.8.3", - "resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz", - "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", - "dev": true, - "requires": { - "ajv": "^4.7.0", - "ajv-keywords": "^1.0.0", - "chalk": "^1.1.1", - "lodash": "^4.0.0", - "slice-ansi": "0.0.4", - "string-width": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", - "dev": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - } - } - }, - "tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz", - "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==", - "dev": true - }, - "text-encoding": { - "version": "0.6.4", - "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "timers-browserify": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", - "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, - "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "optional": true - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "uglifyjs-webpack-plugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", - "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", - "dev": true, - "requires": { - "cacache": "^10.0.4", - "find-cache-dir": "^1.0.0", - "schema-utils": "^0.4.5", - "serialize-javascript": "^1.4.0", - "source-map": "^0.6.1", - "uglify-es": "^3.3.4", - "webpack-sources": "^1.1.0", - "worker-farm": "^1.5.2" - }, - "dependencies": { - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "dev": true, - "requires": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - } - } - } - }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", - "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "unzip": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/unzip/-/unzip-0.1.11.tgz", - "integrity": "sha1-iXScY7BY19kNYZ+GuYqhU107l/A=", - "dev": true, - "requires": { - "binary": ">= 0.3.0 < 1", - "fstream": ">= 0.1.30 < 1", - "match-stream": ">= 0.0.2 < 1", - "pullstream": ">= 0.4.1 < 1", - "readable-stream": "~1.0.31", - "setimmediate": ">= 1.0.1 < 2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-join": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", - "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" - }, - "v8-compile-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz", - "integrity": "sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } - }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", - "dev": true - }, - "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "webpack": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.25.1.tgz", - "integrity": "sha512-T0GU/3NRtO4tMfNzsvpdhUr8HnzA4LTdP2zd+e5zd6CdOH5vNKHnAlO+DvzccfhPdzqRrALOFcjYxx7K5DWmvA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.11", - "@webassemblyjs/helper-module-context": "1.7.11", - "@webassemblyjs/wasm-edit": "1.7.11", - "@webassemblyjs/wasm-parser": "1.7.11", - "acorn": "^5.6.2", - "acorn-dynamic-import": "^3.0.0", - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0", - "chrome-trace-event": "^1.0.0", - "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.0", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.3.0", - "loader-utils": "^1.1.0", - "memory-fs": "~0.4.1", - "micromatch": "^3.1.8", - "mkdirp": "~0.5.0", - "neo-async": "^2.5.0", - "node-libs-browser": "^2.0.0", - "schema-utils": "^0.4.4", - "tapable": "^1.1.0", - "uglifyjs-webpack-plugin": "^1.2.4", - "watchpack": "^1.5.0", - "webpack-sources": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "eslint-scope": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", - "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "webpack-cli": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.1.2.tgz", - "integrity": "sha512-Cnqo7CeqeSvC6PTdts+dywNi5CRlIPbLx1AoUPK2T6vC1YAugMG3IOoO9DmEscd+Dghw7uRlnzV1KwOe5IrtgQ==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "enhanced-resolve": "^4.1.0", - "global-modules-path": "^2.3.0", - "import-local": "^2.0.0", - "interpret": "^1.1.0", - "loader-utils": "^1.1.0", - "supports-color": "^5.5.0", - "v8-compile-cache": "^2.0.2", - "yargs": "^12.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", - "dev": true, - "requires": { - "xregexp": "4.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "xregexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", - "dev": true - }, - "yargs": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", - "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^2.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" - } - } - } - }, - "webpack-log": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz", - "integrity": "sha512-U9AnICnu50HXtiqiDxuli5gLB5PGBo7VvcHx36jRZHwK4vzOYLbImqT4lwWwoMHdQWwEKw736fCHEekokTEKHA==", - "dev": true, - "requires": { - "chalk": "^2.1.0", - "log-symbols": "^2.1.0", - "loglevelnext": "^1.0.1", - "uuid": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "webpack-sources": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", - "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "worker-farm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", - "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - } - } - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", - "dev": true - } - } -} diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json deleted file mode 100644 index ebd2f09a2..000000000 --- a/packages/optimizely-sdk/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@optimizely/optimizely-sdk", - "version": "3.1.0-beta1", - "description": "JavaScript SDK for Optimizely X Full Stack", - "main": "lib/index.node.js", - "browser": "lib/index.browser.js", - "typings": "lib/index.d.ts", - "scripts": { - "test": "mocha ./lib/*.tests.js ./lib/**/*.tests.js ./lib/**/**/*tests.js --recursive --exit", - "test-xbrowser": "karma start karma.bs.conf.js --single-run", - "test-umdbrowser": "npm run build-browser-umd && karma start karma.umd.conf.js --single-run", - "build-browser-umd": "rm -rf dist && webpack", - "test-ci": "npm run test-xbrowser && npm run test-umdbrowser", - "lint": "eslint lib/**", - "cover": "istanbul cover _mocha ./lib/*.tests.js ./lib/**/*.tests.js ./lib/**/**/*tests.js", - "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls", - "prepublishOnly": "npm run build-browser-umd && npm test && npm run test-xbrowser && npm run test-umdbrowser" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/optimizely/javascript-sdk.git" - }, - "license": "Apache-2.0", - "engines": { - "node": ">=4.0.0" - }, - "keywords": [ - "optimizely" - ], - "bugs": { - "url": "https://github.com/optimizely/javascript-sdk/issues" - }, - "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", - "dependencies": { - "@optimizely/js-sdk-logging": "^0.1.0", - "json-schema": "^0.2.3", - "lodash": "^4.0.0", - "murmurhash": "0.0.2", - "sprintf-js": "^1.1.1", - "uuid": "^3.3.2" - }, - "devDependencies": { - "bluebird": "^3.4.6", - "chai": "^4.2.0", - "coveralls": "^3.0.2", - "eslint": "^2.9.0", - "istanbul": "^0.4.5", - "json-loader": "^0.5.4", - "karma": "^3.1.4", - "karma-browserstack-launcher": "^1.2.0", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^2.1.1", - "karma-mocha": "^1.3.0", - "karma-sinon": "^1.0.5", - "karma-webpack": "^3.0.5", - "mocha": "^5.2.0", - "mocha-lcov-reporter": "^1.3.0", - "nock": "^7.7.2", - "sinon": "^2.3.1", - "webpack": "^4.25.1", - "webpack-cli": "^3.1.2" - }, - "publishConfig": { - "access": "public" - }, - "files": [ - "dist/", - "lib/", - "LICENSE", - "CHANGELOG", - "README.md", - "package.json" - ] -} diff --git a/packages/optimizely-sdk/webpack.config.js b/packages/optimizely-sdk/webpack.config.js deleted file mode 100644 index b6a51a5fe..000000000 --- a/packages/optimizely-sdk/webpack.config.js +++ /dev/null @@ -1,24 +0,0 @@ -var path = require('path'); - -module.exports = [ - { - entry: path.resolve(__dirname, 'lib/index.browser.js'), - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'optimizely.browser.umd.js', - library: 'optimizelySdk', - libraryTarget: 'umd', - }, - mode: 'none', - }, - { - entry: path.resolve(__dirname, 'lib/index.browser.js'), - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'optimizely.browser.umd.min.js', - library: 'optimizelySdk', - libraryTarget: 'umd', - }, - mode: 'production', - }, -]; diff --git a/packages/utils/.gitignore b/packages/utils/.gitignore deleted file mode 100644 index d4a125901..000000000 --- a/packages/utils/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -lib/ -doc/ diff --git a/packages/utils/CHANGELOG.MD b/packages/utils/CHANGELOG.MD deleted file mode 100644 index dbf1b2562..000000000 --- a/packages/utils/CHANGELOG.MD +++ /dev/null @@ -1,12 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## [Unreleased] -Changes that have landed but are not yet released. - -## [0.1.0] - March 1, 2019 - -Initial release \ No newline at end of file diff --git a/packages/utils/LICENSE b/packages/utils/LICENSE deleted file mode 100644 index b9f80c5bd..000000000 --- a/packages/utils/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2016-2017, Optimizely, Inc. and contributors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/utils/README.md b/packages/utils/README.md deleted file mode 100644 index d9bb67bbc..000000000 --- a/packages/utils/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `@optimizely/js-sdk-utils` - -A collection of utility functions shared between components of the Javascript SDK. - -### To test - -``` -npm test -``` \ No newline at end of file diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js deleted file mode 100644 index 391afe8eb..000000000 --- a/packages/utils/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - // "roots": [ - // "./src" - // ], - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ], -} \ No newline at end of file diff --git a/packages/utils/package-lock.json b/packages/utils/package-lock.json deleted file mode 100644 index 1c4eac181..000000000 --- a/packages/utils/package-lock.json +++ /dev/null @@ -1,5571 +0,0 @@ -{ - "name": "@optimizely/js-sdk-utils", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - } - } - }, - "@types/jest": { - "version": "23.3.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.14.tgz", - "integrity": "sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug==", - "dev": true - }, - "@types/node": { - "version": "11.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.4.tgz", - "integrity": "sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==", - "dev": true - }, - "@types/uuid": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", - "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", - "dev": true - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", - "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz", - "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==", - "dev": true - } - } - }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", - "dev": true - }, - "ajv": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", - "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", - "dev": true, - "requires": { - "default-require-extensions": "^1.0.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz", - "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", - "dev": true, - "requires": { - "babel-plugin-istanbul": "^4.1.6", - "babel-preset-jest": "^23.2.0" - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-istanbul": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", - "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.13.0", - "find-up": "^2.1.0", - "istanbul-lib-instrument": "^1.10.1", - "test-exclude": "^4.2.1" - } - }, - "babel-plugin-jest-hoist": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz", - "integrity": "sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-preset-jest": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz", - "integrity": "sha1-jsegOhOPABoaj7HoETZSvxpV2kY=", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^23.2.0", - "babel-plugin-syntax-object-rest-spread": "^6.13.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "capture-exit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", - "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", - "dev": true, - "requires": { - "rsvp": "^3.3.3" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true, - "optional": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", - "dev": true - }, - "cssstyle": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", - "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", - "dev": true, - "requires": { - "strip-bom": "^2.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "dev": true, - "requires": { - "merge": "^1.2.0" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } - }, - "expect": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-23.6.0.tgz", - "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "jest-diff": "^23.6.0", - "jest-get-type": "^22.1.0", - "jest-matcher-utils": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-regex-util": "^23.3.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "dev": true, - "requires": { - "bser": "^2.0.0" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", - "dev": true, - "requires": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true - }, - "handlebars": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", - "dev": true, - "requires": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", - "dev": true, - "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-1.0.0.tgz", - "integrity": "sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-api": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", - "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", - "dev": true, - "requires": { - "async": "^2.1.4", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.1", - "istanbul-lib-hook": "^1.2.2", - "istanbul-lib-instrument": "^1.10.2", - "istanbul-lib-report": "^1.1.5", - "istanbul-lib-source-maps": "^1.2.6", - "istanbul-reports": "^1.5.1", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", - "once": "^1.4.0" - } - }, - "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", - "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", - "dev": true, - "requires": { - "append-transform": "^0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": true, - "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" - } - }, - "istanbul-lib-report": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", - "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", - "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "istanbul-reports": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", - "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", - "dev": true, - "requires": { - "handlebars": "^4.0.3" - } - }, - "jest": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", - "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", - "dev": true, - "requires": { - "import-local": "^1.0.0", - "jest-cli": "^23.6.0" - }, - "dependencies": { - "jest-cli": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", - "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "import-local": "^1.0.0", - "is-ci": "^1.0.10", - "istanbul-api": "^1.3.1", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-instrument": "^1.10.1", - "istanbul-lib-source-maps": "^1.2.4", - "jest-changed-files": "^23.4.2", - "jest-config": "^23.6.0", - "jest-environment-jsdom": "^23.4.0", - "jest-get-type": "^22.1.0", - "jest-haste-map": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-regex-util": "^23.3.0", - "jest-resolve-dependencies": "^23.6.0", - "jest-runner": "^23.6.0", - "jest-runtime": "^23.6.0", - "jest-snapshot": "^23.6.0", - "jest-util": "^23.4.0", - "jest-validate": "^23.6.0", - "jest-watcher": "^23.4.0", - "jest-worker": "^23.2.0", - "micromatch": "^2.3.11", - "node-notifier": "^5.2.1", - "prompts": "^0.1.9", - "realpath-native": "^1.0.0", - "rimraf": "^2.5.4", - "slash": "^1.0.0", - "string-length": "^2.0.0", - "strip-ansi": "^4.0.0", - "which": "^1.2.12", - "yargs": "^11.0.0" - } - } - } - }, - "jest-changed-files": { - "version": "23.4.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-23.4.2.tgz", - "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", - "dev": true, - "requires": { - "throat": "^4.0.0" - } - }, - "jest-config": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.6.0.tgz", - "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-jest": "^23.6.0", - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^23.4.0", - "jest-environment-node": "^23.4.0", - "jest-get-type": "^22.1.0", - "jest-jasmine2": "^23.6.0", - "jest-regex-util": "^23.3.0", - "jest-resolve": "^23.6.0", - "jest-util": "^23.4.0", - "jest-validate": "^23.6.0", - "micromatch": "^2.3.11", - "pretty-format": "^23.6.0" - } - }, - "jest-diff": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", - "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "diff": "^3.2.0", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" - } - }, - "jest-docblock": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-23.2.0.tgz", - "integrity": "sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - }, - "jest-each": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-23.6.0.tgz", - "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "pretty-format": "^23.6.0" - } - }, - "jest-environment-jsdom": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz", - "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", - "dev": true, - "requires": { - "jest-mock": "^23.2.0", - "jest-util": "^23.4.0", - "jsdom": "^11.5.1" - } - }, - "jest-environment-node": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-23.4.0.tgz", - "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", - "dev": true, - "requires": { - "jest-mock": "^23.2.0", - "jest-util": "^23.4.0" - } - }, - "jest-get-type": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", - "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", - "dev": true - }, - "jest-haste-map": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-23.6.0.tgz", - "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", - "dev": true, - "requires": { - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.1.11", - "invariant": "^2.2.4", - "jest-docblock": "^23.2.0", - "jest-serializer": "^23.0.1", - "jest-worker": "^23.2.0", - "micromatch": "^2.3.11", - "sane": "^2.0.0" - } - }, - "jest-jasmine2": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz", - "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", - "dev": true, - "requires": { - "babel-traverse": "^6.0.0", - "chalk": "^2.0.1", - "co": "^4.6.0", - "expect": "^23.6.0", - "is-generator-fn": "^1.0.0", - "jest-diff": "^23.6.0", - "jest-each": "^23.6.0", - "jest-matcher-utils": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-snapshot": "^23.6.0", - "jest-util": "^23.4.0", - "pretty-format": "^23.6.0" - } - }, - "jest-leak-detector": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz", - "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", - "dev": true, - "requires": { - "pretty-format": "^23.6.0" - } - }, - "jest-matcher-utils": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz", - "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" - } - }, - "jest-message-util": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", - "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0-beta.35", - "chalk": "^2.0.1", - "micromatch": "^2.3.11", - "slash": "^1.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-23.2.0.tgz", - "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", - "dev": true - }, - "jest-regex-util": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", - "integrity": "sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=", - "dev": true - }, - "jest-resolve": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-23.6.0.tgz", - "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", - "dev": true, - "requires": { - "browser-resolve": "^1.11.3", - "chalk": "^2.0.1", - "realpath-native": "^1.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz", - "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", - "dev": true, - "requires": { - "jest-regex-util": "^23.3.0", - "jest-snapshot": "^23.6.0" - } - }, - "jest-runner": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-23.6.0.tgz", - "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "graceful-fs": "^4.1.11", - "jest-config": "^23.6.0", - "jest-docblock": "^23.2.0", - "jest-haste-map": "^23.6.0", - "jest-jasmine2": "^23.6.0", - "jest-leak-detector": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-runtime": "^23.6.0", - "jest-util": "^23.4.0", - "jest-worker": "^23.2.0", - "source-map-support": "^0.5.6", - "throat": "^4.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - } - } - }, - "jest-runtime": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-23.6.0.tgz", - "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", - "dev": true, - "requires": { - "babel-core": "^6.0.0", - "babel-plugin-istanbul": "^4.1.6", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "exit": "^0.1.2", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.11", - "jest-config": "^23.6.0", - "jest-haste-map": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-regex-util": "^23.3.0", - "jest-resolve": "^23.6.0", - "jest-snapshot": "^23.6.0", - "jest-util": "^23.4.0", - "jest-validate": "^23.6.0", - "micromatch": "^2.3.11", - "realpath-native": "^1.0.0", - "slash": "^1.0.0", - "strip-bom": "3.0.0", - "write-file-atomic": "^2.1.0", - "yargs": "^11.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "jest-serializer": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-23.0.1.tgz", - "integrity": "sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU=", - "dev": true - }, - "jest-snapshot": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-23.6.0.tgz", - "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", - "dev": true, - "requires": { - "babel-types": "^6.0.0", - "chalk": "^2.0.1", - "jest-diff": "^23.6.0", - "jest-matcher-utils": "^23.6.0", - "jest-message-util": "^23.4.0", - "jest-resolve": "^23.6.0", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^23.6.0", - "semver": "^5.5.0" - } - }, - "jest-util": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-23.4.0.tgz", - "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", - "dev": true, - "requires": { - "callsites": "^2.0.0", - "chalk": "^2.0.1", - "graceful-fs": "^4.1.11", - "is-ci": "^1.0.10", - "jest-message-util": "^23.4.0", - "mkdirp": "^0.5.1", - "slash": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "jest-validate": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz", - "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^22.1.0", - "leven": "^2.1.0", - "pretty-format": "^23.6.0" - } - }, - "jest-watcher": { - "version": "23.4.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-23.4.0.tgz", - "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "string-length": "^2.0.0" - } - }, - "jest-worker": { - "version": "23.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz", - "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", - "dev": true, - "requires": { - "merge-stream": "^1.0.1" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "js-yaml": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.1", - "escodegen": "^1.9.1", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", - "xml-name-validator": "^3.0.0" - } - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "kleur": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-2.0.2.tgz", - "integrity": "sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==", - "dev": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", - "dev": true - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", - "dev": true - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "dev": true - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", - "dev": true - }, - "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", - "dev": true, - "requires": { - "mime-db": "~1.38.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nan": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-notifier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", - "dev": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz", - "integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", - "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true, - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "prompts": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-0.1.14.tgz", - "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", - "dev": true, - "requires": { - "kleur": "^2.0.1", - "sisteransi": "^0.1.1" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "realpath-native": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", - "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", - "dev": true, - "requires": { - "util.promisify": "^1.0.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "dev": true, - "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", - "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "capture-exit": "^1.2.0", - "exec-sh": "^0.2.0", - "fb-watchman": "^2.0.0", - "fsevents": "^1.2.3", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5", - "watch": "~0.18.0" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "sisteransi": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz", - "integrity": "sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "dev": true, - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true - }, - "test-exclude": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz", - "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "micromatch": "^2.3.11", - "object-assign": "^4.1.0", - "read-pkg-up": "^1.0.1", - "require-main-filename": "^1.0.1" - } - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", - "dev": true - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "ts-jest": { - "version": "23.10.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.10.5.tgz", - "integrity": "sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A==", - "dev": true, - "requires": { - "bs-logger": "0.x", - "buffer-from": "1.x", - "fast-json-stable-stringify": "2.x", - "json5": "2.x", - "make-error": "1.x", - "mkdirp": "0.x", - "resolve": "1.x", - "semver": "^5.5", - "yargs-parser": "10.x" - }, - "dependencies": { - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "dev": true, - "requires": { - "makeerror": "1.0.x" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "dev": true, - "requires": { - "exec-sh": "^0.2.0", - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, - "yargs": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", - "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" - } - }, - "yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } - } - } -} diff --git a/packages/utils/package.json b/packages/utils/package.json deleted file mode 100644 index 51d1097f3..000000000 --- a/packages/utils/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@optimizely/js-sdk-utils", - "version": "0.1.0", - "description": "Optimizely Full Stack Utils", - "author": "jordangarcia <jordan@optimizely.com>", - "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/utils", - "license": "MIT", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "directories": { - "lib": "lib", - "test": "test" - }, - "files": [ - "lib", - "LICENSE", - "CHANGELOG", - "README.md", - "package.json" - ], - "scripts": { - "tsc": "rm -rf lib && tsc", - "test": "jest", - "prepublishOnly": "jest && npm run tsc" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/optimizely/javascript-sdk.git" - }, - "keywords": [ - "optimizely" - ], - "bugs": { - "url": "https://github.com/optimizely/javascript-sdk/issues" - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "uuid": "^3.3.2" - }, - "devDependencies": { - "@types/jest": "^23.3.12", - "@types/uuid": "^3.4.4", - "jest": "^23.6.0", - "ts-jest": "^23.10.5" - } -} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts deleted file mode 100644 index 18a227f67..000000000 --- a/packages/utils/src/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright 2019, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { v4 } from 'uuid' - -export type Omit<T, K> = Pick<T, Exclude<keyof T, K>> - -export function getTimestamp(): number { - return new Date().getTime() -} - -export function generateUUID(): string { - return v4() -} - -/** - * Validates a value is a valid TypeScript enum - * - * @export - * @param {object} enumToCheck - * @param {*} value - * @returns {boolean} - */ -export function isValidEnum(enumToCheck: { [key: string]: any }, value: any): boolean { - let found = false - - const keys = Object.keys(enumToCheck) - for (let index = 0; index < keys.length; index++) { - if (value === enumToCheck[keys[index]]) { - found = true - break - } - } - return found -} - -export function groupBy<K>(arr: K[], grouperFn: (item: K) => string): Array<K[]> { - const grouper: { [key: string]: K[] } = {} - - arr.forEach(item => { - const key = grouperFn(item) - grouper[key] = grouper[key] || [] - grouper[key].push(item) - }) - - return objectValues(grouper) -} - -export function objectValues<K>(obj: { [key: string]: K }): K[] { - return Object.keys(obj).map(key => obj[key]) -} - -export function find<K>(arr: K[], cond: (arg: K) => boolean): K | undefined { - let found - - for (let item of arr) { - if (cond(item)) { - found = item - break - } - } - - return found -} - -export function keyBy<K>(arr: K[], keyByFn: (item: K) => string): { [key: string]: K } { - let map: { [key: string]: K } = {} - arr.forEach(item => { - const key = keyByFn(item) - map[key] = item - }) - return map -} - -export function sprintf(format: string, ...args: any[]): string { - var i = 0 - return format.replace(/%s/g, function() { - const arg = args[i++] - const type = typeof arg - if (type === 'function') { - return arg() - } else if (type === 'string') { - return arg - } else { - return String(arg) - } - }) -} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json deleted file mode 100644 index 408b0e9a6..000000000 --- a/packages/utils/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./lib" - }, - "include": [ - "./src" - ], - "exclude": [ - "./lib", - "./src/**/*.spec.ts" - ] -} \ No newline at end of file diff --git a/packages/utils/yarn.lock b/packages/utils/yarn.lock deleted file mode 100644 index ca094a7bc..000000000 --- a/packages/utils/yarn.lock +++ /dev/null @@ -1,3684 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0-beta.35": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" - integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/highlight@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" - integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - -"@types/jest@^23.3.12": - version "23.3.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a" - integrity sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug== - -"@types/node@*": - version "11.9.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14" - integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA== - -"@types/uuid@^3.4.4": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5" - integrity sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw== - dependencies: - "@types/node" "*" - -abab@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" - integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn-globals@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" - integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - -acorn-walk@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" - integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== - -acorn@^5.5.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" - integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== - -acorn@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" - integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== - -ajv@^6.5.5: - version "6.9.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1" - integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -append-transform@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" - integrity sha1-126/jKlNJ24keja61EpLdKthGZE= - dependencies: - default-require-extensions "^1.0.0" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= - dependencies: - arr-flatten "^1.0.1" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== - -async@^2.1.4, async@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" - integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== - dependencies: - lodash "^4.17.11" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.0.0, babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-generator@^6.18.0, babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-jest@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-23.6.0.tgz#a644232366557a2240a0c083da6b25786185a2f1" - integrity sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew== - dependencies: - babel-plugin-istanbul "^4.1.6" - babel-preset-jest "^23.2.0" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-istanbul@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" - integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ== - dependencies: - babel-plugin-syntax-object-rest-spread "^6.13.0" - find-up "^2.1.0" - istanbul-lib-instrument "^1.10.1" - test-exclude "^4.2.1" - -babel-plugin-jest-hoist@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" - integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc= - -babel-plugin-syntax-object-rest-spread@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" - integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= - -babel-preset-jest@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46" - integrity sha1-jsegOhOPABoaj7HoETZSvxpV2kY= - dependencies: - babel-plugin-jest-hoist "^23.2.0" - babel-plugin-syntax-object-rest-spread "^6.13.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -browser-process-hrtime@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" - integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== - -browser-resolve@^1.11.3: - version "1.11.3" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" - integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== - dependencies: - resolve "1.1.7" - -bs-logger@0.x: - version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" - integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== - dependencies: - fast-json-stable-stringify "2.x" - -bser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" - integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk= - dependencies: - node-int64 "^0.4.0" - -buffer-from@1.x, buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -capture-exit@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" - integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= - dependencies: - rsvp "^3.3.3" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" - integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== - -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== - dependencies: - delayed-stream "~1.0.0" - -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -component-emitter@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -convert-source-map@^1.4.0, convert-source-map@^1.5.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== - dependencies: - safe-buffer "~5.1.1" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" - integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": - version "0.3.6" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad" - integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A== - -cssstyle@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.2.1.tgz#3aceb2759eaf514ac1a21628d723d6043a819495" - integrity sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A== - dependencies: - cssom "0.3.x" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -data-urls@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - -debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -decamelize@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -default-require-extensions@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" - integrity sha1-836hXT4T/9m0N9M+GnW1+5eHTLg= - dependencies: - strip-bom "^2.0.0" - -define-properties@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -detect-newline@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.5.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-keys "^1.0.12" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escodegen@^1.9.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" - integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -estraverse@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= - -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== - dependencies: - merge "^1.2.0" - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= - dependencies: - is-posix-bracket "^0.1.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= - dependencies: - fill-range "^2.1.0" - -expect@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98" - integrity sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w== - dependencies: - ansi-styles "^3.2.0" - jest-diff "^23.6.0" - jest-get-type "^22.1.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= - dependencies: - is-extglob "^1.0.0" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -fast-levenshtein@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fb-watchman@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" - integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= - dependencies: - bser "^2.0.0" - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= - -fileset@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" - integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= - dependencies: - glob "^7.0.3" - minimatch "^3.0.3" - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fs-minipass@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" - integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== - dependencies: - minipass "^2.2.1" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4" - integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw== - dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= - dependencies: - is-glob "^2.0.0" - -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" - integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== - -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= - -handlebars@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" - integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== - dependencies: - async "^2.5.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hosted-git-info@^2.1.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" - integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== - -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -iconv-lite@0.4.24, iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== - dependencies: - minimatch "^3.0.4" - -import-local@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" - integrity sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ== - dependencies: - pkg-dir "^2.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-generator-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" - integrity sha1-lp1J4bszKfa7fwkIm+JleLLd1Go= - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= - dependencies: - is-extglob "^1.0.0" - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -istanbul-api@^1.3.1: - version "1.3.7" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa" - integrity sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA== - dependencies: - async "^2.1.4" - fileset "^2.0.2" - istanbul-lib-coverage "^1.2.1" - istanbul-lib-hook "^1.2.2" - istanbul-lib-instrument "^1.10.2" - istanbul-lib-report "^1.1.5" - istanbul-lib-source-maps "^1.2.6" - istanbul-reports "^1.5.1" - js-yaml "^3.7.0" - mkdirp "^0.5.1" - once "^1.4.0" - -istanbul-lib-coverage@^1.2.0, istanbul-lib-coverage@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" - integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== - -istanbul-lib-hook@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86" - integrity sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw== - dependencies: - append-transform "^0.4.0" - -istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" - integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.18.0" - istanbul-lib-coverage "^1.2.1" - semver "^5.3.0" - -istanbul-lib-report@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c" - integrity sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw== - dependencies: - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - path-parse "^1.0.5" - supports-color "^3.1.2" - -istanbul-lib-source-maps@^1.2.4, istanbul-lib-source-maps@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" - integrity sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg== - dependencies: - debug "^3.1.0" - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - rimraf "^2.6.1" - source-map "^0.5.3" - -istanbul-reports@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a" - integrity sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw== - dependencies: - handlebars "^4.0.3" - -jest-changed-files@^23.4.2: - version "23.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" - integrity sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA== - dependencies: - throat "^4.0.0" - -jest-cli@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.6.0.tgz#61ab917744338f443ef2baa282ddffdd658a5da4" - integrity sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.1.11" - import-local "^1.0.0" - is-ci "^1.0.10" - istanbul-api "^1.3.1" - istanbul-lib-coverage "^1.2.0" - istanbul-lib-instrument "^1.10.1" - istanbul-lib-source-maps "^1.2.4" - jest-changed-files "^23.4.2" - jest-config "^23.6.0" - jest-environment-jsdom "^23.4.0" - jest-get-type "^22.1.0" - jest-haste-map "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - jest-resolve-dependencies "^23.6.0" - jest-runner "^23.6.0" - jest-runtime "^23.6.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - jest-watcher "^23.4.0" - jest-worker "^23.2.0" - micromatch "^2.3.11" - node-notifier "^5.2.1" - prompts "^0.1.9" - realpath-native "^1.0.0" - rimraf "^2.5.4" - slash "^1.0.0" - string-length "^2.0.0" - strip-ansi "^4.0.0" - which "^1.2.12" - yargs "^11.0.0" - -jest-config@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.6.0.tgz#f82546a90ade2d8c7026fbf6ac5207fc22f8eb1d" - integrity sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ== - dependencies: - babel-core "^6.0.0" - babel-jest "^23.6.0" - chalk "^2.0.1" - glob "^7.1.1" - jest-environment-jsdom "^23.4.0" - jest-environment-node "^23.4.0" - jest-get-type "^22.1.0" - jest-jasmine2 "^23.6.0" - jest-regex-util "^23.3.0" - jest-resolve "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - micromatch "^2.3.11" - pretty-format "^23.6.0" - -jest-diff@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d" - integrity sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g== - dependencies: - chalk "^2.0.1" - diff "^3.2.0" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-docblock@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-23.2.0.tgz#f085e1f18548d99fdd69b20207e6fd55d91383a7" - integrity sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c= - dependencies: - detect-newline "^2.1.0" - -jest-each@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz#ba0c3a82a8054387016139c733a05242d3d71575" - integrity sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg== - dependencies: - chalk "^2.0.1" - pretty-format "^23.6.0" - -jest-environment-jsdom@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz#056a7952b3fea513ac62a140a2c368c79d9e6023" - integrity sha1-BWp5UrP+pROsYqFAosNox52eYCM= - dependencies: - jest-mock "^23.2.0" - jest-util "^23.4.0" - jsdom "^11.5.1" - -jest-environment-node@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-23.4.0.tgz#57e80ed0841dea303167cce8cd79521debafde10" - integrity sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA= - dependencies: - jest-mock "^23.2.0" - jest-util "^23.4.0" - -jest-get-type@^22.1.0: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" - integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== - -jest-haste-map@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.6.0.tgz#2e3eb997814ca696d62afdb3f2529f5bbc935e16" - integrity sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg== - dependencies: - fb-watchman "^2.0.0" - graceful-fs "^4.1.11" - invariant "^2.2.4" - jest-docblock "^23.2.0" - jest-serializer "^23.0.1" - jest-worker "^23.2.0" - micromatch "^2.3.11" - sane "^2.0.0" - -jest-jasmine2@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz#840e937f848a6c8638df24360ab869cc718592e0" - integrity sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ== - dependencies: - babel-traverse "^6.0.0" - chalk "^2.0.1" - co "^4.6.0" - expect "^23.6.0" - is-generator-fn "^1.0.0" - jest-diff "^23.6.0" - jest-each "^23.6.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - pretty-format "^23.6.0" - -jest-leak-detector@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz#e4230fd42cf381a1a1971237ad56897de7e171de" - integrity sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg== - dependencies: - pretty-format "^23.6.0" - -jest-matcher-utils@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80" - integrity sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog== - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-message-util@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-23.4.0.tgz#17610c50942349508d01a3d1e0bda2c079086a9f" - integrity sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8= - dependencies: - "@babel/code-frame" "^7.0.0-beta.35" - chalk "^2.0.1" - micromatch "^2.3.11" - slash "^1.0.0" - stack-utils "^1.0.1" - -jest-mock@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-23.2.0.tgz#ad1c60f29e8719d47c26e1138098b6d18b261134" - integrity sha1-rRxg8p6HGdR8JuETgJi20YsmETQ= - -jest-regex-util@^23.3.0: - version "23.3.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-23.3.0.tgz#5f86729547c2785c4002ceaa8f849fe8ca471bc5" - integrity sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U= - -jest-resolve-dependencies@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz#b4526af24c8540d9a3fab102c15081cf509b723d" - integrity sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA== - dependencies: - jest-regex-util "^23.3.0" - jest-snapshot "^23.6.0" - -jest-resolve@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.6.0.tgz#cf1d1a24ce7ee7b23d661c33ba2150f3aebfa0ae" - integrity sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA== - dependencies: - browser-resolve "^1.11.3" - chalk "^2.0.1" - realpath-native "^1.0.0" - -jest-runner@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.6.0.tgz#3894bd219ffc3f3cb94dc48a4170a2e6f23a5a38" - integrity sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA== - dependencies: - exit "^0.1.2" - graceful-fs "^4.1.11" - jest-config "^23.6.0" - jest-docblock "^23.2.0" - jest-haste-map "^23.6.0" - jest-jasmine2 "^23.6.0" - jest-leak-detector "^23.6.0" - jest-message-util "^23.4.0" - jest-runtime "^23.6.0" - jest-util "^23.4.0" - jest-worker "^23.2.0" - source-map-support "^0.5.6" - throat "^4.0.0" - -jest-runtime@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.6.0.tgz#059e58c8ab445917cd0e0d84ac2ba68de8f23082" - integrity sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw== - dependencies: - babel-core "^6.0.0" - babel-plugin-istanbul "^4.1.6" - chalk "^2.0.1" - convert-source-map "^1.4.0" - exit "^0.1.2" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.1.11" - jest-config "^23.6.0" - jest-haste-map "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - jest-resolve "^23.6.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - micromatch "^2.3.11" - realpath-native "^1.0.0" - slash "^1.0.0" - strip-bom "3.0.0" - write-file-atomic "^2.1.0" - yargs "^11.0.0" - -jest-serializer@^23.0.1: - version "23.0.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-23.0.1.tgz#a3776aeb311e90fe83fab9e533e85102bd164165" - integrity sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU= - -jest-snapshot@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.6.0.tgz#f9c2625d1b18acda01ec2d2b826c0ce58a5aa17a" - integrity sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg== - dependencies: - babel-types "^6.0.0" - chalk "^2.0.1" - jest-diff "^23.6.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-resolve "^23.6.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^23.6.0" - semver "^5.5.0" - -jest-util@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-23.4.0.tgz#4d063cb927baf0a23831ff61bec2cbbf49793561" - integrity sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE= - dependencies: - callsites "^2.0.0" - chalk "^2.0.1" - graceful-fs "^4.1.11" - is-ci "^1.0.10" - jest-message-util "^23.4.0" - mkdirp "^0.5.1" - slash "^1.0.0" - source-map "^0.6.0" - -jest-validate@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" - integrity sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A== - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - leven "^2.1.0" - pretty-format "^23.6.0" - -jest-watcher@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-23.4.0.tgz#d2e28ce74f8dad6c6afc922b92cabef6ed05c91c" - integrity sha1-0uKM50+NrWxq/JIrksq+9u0FyRw= - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - string-length "^2.0.0" - -jest-worker@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-23.2.0.tgz#faf706a8da36fae60eb26957257fa7b5d8ea02b9" - integrity sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk= - dependencies: - merge-stream "^1.0.1" - -jest@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-23.6.0.tgz#ad5835e923ebf6e19e7a1d7529a432edfee7813d" - integrity sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw== - dependencies: - import-local "^1.0.0" - jest-cli "^23.6.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.7.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsdom@^11.5.1: - version "11.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json5@2.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" - integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== - dependencies: - minimist "^1.2.0" - -json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" - integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== - -kleur@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" - integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - -leven@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" - integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= - -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash@^4.17.11, lodash@^4.17.4: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-error@1.x: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" - integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== - -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= - dependencies: - tmpl "1.0.x" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" - integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= - dependencies: - mimic-fn "^1.0.0" - -merge-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" - integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= - dependencies: - readable-stream "^2.0.1" - -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - -micromatch@^2.3.11: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -mime-db@~1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.22" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" - integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== - dependencies: - mime-db "~1.38.0" - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -minimatch@^3.0.3, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.1.1, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - -minipass@^2.2.1, minipass@^2.3.4: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== - dependencies: - minipass "^2.2.1" - -mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" - integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" - integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== - dependencies: - debug "^2.1.2" - iconv-lite "^0.4.4" - sax "^1.2.4" - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= - -node-notifier@^5.2.1: - version "5.4.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.0.tgz#7b455fdce9f7de0c63538297354f3db468426e6a" - integrity sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ== - dependencies: - growly "^1.3.0" - is-wsl "^1.1.0" - semver "^5.5.0" - shellwords "^0.1.1" - which "^1.3.0" - -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -npm-bundled@^1.0.1: - version "1.0.6" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" - integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== - -npm-packlist@^1.1.6: - version "1.4.1" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc" - integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -nwsapi@^2.0.7: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.0.tgz#781065940aed90d9bb01ca5d0ce0fcf81c32712f" - integrity sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-keys@^1.0.12: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032" - integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -once@^1.3.0, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA== - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.5, path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= - -pretty-format@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" - integrity sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw== - dependencies: - ansi-regex "^3.0.0" - ansi-styles "^3.2.0" - -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== - -prompts@^0.1.9: - version "0.1.14" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2" - integrity sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w== - dependencies: - kleur "^2.0.1" - sisteransi "^0.1.1" - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -psl@^1.1.24, psl@^1.1.28: - version "1.1.31" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" - integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.1, readable-stream@^2.0.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -realpath-native@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== - dependencies: - is-equal-shallow "^0.1.3" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request-promise-core@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" - integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== - dependencies: - lodash "^4.17.11" - -request-promise-native@^1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" - integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== - dependencies: - request-promise-core "1.1.2" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.87.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@1.x, resolve@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== - dependencies: - path-parse "^1.0.6" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rimraf@^2.5.4, rimraf@^2.6.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rsvp@^3.3.3: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" - integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== - -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sane@^2.0.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" - integrity sha1-tNwYYcIbQn6SlQej51HiosuKs/o= - dependencies: - anymatch "^2.0.0" - capture-exit "^1.2.0" - exec-sh "^0.2.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - watch "~0.18.0" - optionalDependencies: - fsevents "^1.2.3" - -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5, semver@^5.5.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" - -set-value@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" - integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -sisteransi@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" - integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-map-resolve@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" - integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== - dependencies: - atob "^2.1.1" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-support@^0.5.6: - version "0.5.10" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" - integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" - integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -stack-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" - integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - -string-length@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" - integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= - dependencies: - astral-regex "^1.0.0" - strip-ansi "^4.0.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-bom@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.1.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -symbol-tree@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" - integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= - -tar@^4: - version "4.4.8" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" - integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.4" - minizlib "^1.1.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.2" - -test-exclude@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" - integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== - dependencies: - arrify "^1.0.1" - micromatch "^2.3.11" - object-assign "^4.1.0" - read-pkg-up "^1.0.1" - require-main-filename "^1.0.1" - -throat@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" - integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= - -tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -tough-cookie@^2.3.3, tough-cookie@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -ts-jest@^23.10.5: - version "23.10.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.10.5.tgz#cdb550df4466a30489bf70ba867615799f388dd5" - integrity sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "10.x" - -tslib@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" - integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== - dependencies: - commander "~2.17.1" - source-map "~0.6.1" - -union-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" - integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^0.4.3" - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - -uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -w3c-hr-time@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" - integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= - dependencies: - browser-process-hrtime "^0.1.2" - -walker@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= - dependencies: - makeerror "1.0.x" - -watch@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" - integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= - dependencies: - exec-sh "^0.2.0" - minimist "^1.2.0" - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - -whatwg-url@^6.4.1: - version "6.5.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -whatwg-url@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" - integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.12, which@^1.2.9, which@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^2.1.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.2.tgz#a7181706dfba17855d221140a9c06e15fcdd87b9" - integrity sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -ws@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" - integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== - dependencies: - async-limiter "~1.0.0" - -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yallist@^3.0.0, yallist@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== - -yargs-parser@10.x: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - -yargs-parser@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" - integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc= - dependencies: - camelcase "^4.1.0" - -yargs@^11.0.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" - integrity sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A== - dependencies: - cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^9.0.2" diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 000000000..f7cc6c247 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,216 @@ +/** + * Copyright 2020-2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import commonjs from '@rollup/plugin-commonjs'; +import { terser } from 'rollup-plugin-terser'; +import resolve from '@rollup/plugin-node-resolve'; +import { dependencies, peerDependencies } from './package.json'; +import typescript from 'rollup-plugin-typescript2'; + +const typescriptPluginOptions = { + allowJs: true, + exclude: [ + './dist', + './lib/**/*.tests.js', + './lib/**/*.tests.ts', + './lib/**/*.umdtests.js', + './lib/tests', + 'node_modules', + ], + include: ['./lib/**/*.ts', './lib/**/*.js'], + tsconfigOverride: { + compilerOptions: { + paths: { + "*": [ + "./typings/*" + ], + "error_message": [ + "./lib/message/error_message.gen" + ], + "log_message": [ + "./lib/message/log_message.gen" + ], + } + } + } +}; + +const cjsBundleFor = (platform, opt = {}) => { + const { minify, ext } = { + minify: true, + ext: '.js', + ...opt, + }; + + const min = minify ? '.min' : ''; + + return { + plugins: [resolve(), commonjs(), typescript(typescriptPluginOptions)], + external: ['https', 'http', 'url'].concat(Object.keys({ ...dependencies, ...peerDependencies } || {})), + input: `lib/index.${platform}.ts`, + output: { + exports: 'named', + format: 'cjs', + file: `dist/index.${platform}${min}${ext}`, + plugins: minify ? [terser()] : undefined, + sourcemap: true, + }, + } +}; + +const esmBundleFor = (platform, opt) => { + const { minify, ext } = { + minify: true, + ext: '.js', + ...opt, + }; + + const min = minify ? '.min' : ''; + + return { + ...cjsBundleFor(platform), + output: [ + { + format: 'es', + file: `dist/index.${platform}.es${min}${ext}`, + plugins: minify ? [terser()] : undefined, + sourcemap: true, + }, + ], + } +}; + +const cjsBundleForUAParser = (opt = {}) => { + const { minify, ext } = { + minify: true, + ext: '.js', + ...opt, + }; + + const min = minify ? '.min' : ''; + + return { + plugins: [resolve(), commonjs(), typescript(typescriptPluginOptions)], + external: ['https', 'http', 'url'].concat(Object.keys({ ...dependencies, ...peerDependencies } || {})), + input: `lib/odp/ua_parser/ua_parser.ts`, + output: { + exports: 'named', + format: 'cjs', + file: `dist/ua_parser${min}${ext}`, + plugins: minify ? [terser()] : undefined, + sourcemap: true, + }, + }; +}; + +const esmBundleForUAParser = (opt = {}) => { + const { minify, ext } = { + minify: true, + ext: '.js', + ...opt, + }; + + const min = minify ? '.min' : ''; + + return { + ...cjsBundleForUAParser(), + output: [ + { + format: 'es', + file: `dist/ua_parser.es${min}${ext}`, + plugins: minify ? [terser()] : undefined, + sourcemap: true, + }, + ], + }; +}; + +const umdBundle = { + plugins: [ + resolve({ browser: true }), + commonjs({ + namedExports: { + '@optimizely/js-sdk-event-processor': ['LogTierV1EventProcessor', 'LocalStoragePendingEventsDispatcher'], + 'json-schema': ['validate'], + }, + }), + typescript(typescriptPluginOptions), + ], + input: 'lib/index.browser.ts', + output: [ + { + name: 'optimizelySdk', + format: 'umd', + file: 'dist/optimizely.browser.umd.js', + exports: 'named', + }, + { + name: 'optimizelySdk', + format: 'umd', + file: 'dist/optimizely.browser.umd.min.js', + exports: 'named', + plugins: [terser()], + sourcemap: true, + }, + ], +}; + +// A separate bundle for json schema validator. +const jsonSchemaBundle = { + plugins: [resolve(), commonjs(), typescript(typescriptPluginOptions)], + external: ['json-schema', 'uuid'], + input: 'lib/utils/json_schema_validator/index.ts', + output: { + exports: 'named', + format: 'cjs', + file: 'dist/optimizely.json_schema_validator.min.js', + plugins: [terser()], + sourcemap: true, + }, +}; + +const bundles = { + 'cjs-node-min': cjsBundleFor('node'), + 'cjs-browser-min': cjsBundleFor('browser'), + 'cjs-react-native-min': cjsBundleFor('react_native'), + 'cjs-universal': cjsBundleFor('universal'), + 'esm-browser-min': esmBundleFor('browser'), + 'esm-node-min': esmBundleFor('node', { ext: '.mjs' }), + 'esm-react-native-min': esmBundleFor('react_native'), + 'esm-universal': esmBundleFor('universal'), + 'json-schema': jsonSchemaBundle, + 'cjs-ua-parser-min': cjsBundleForUAParser(), + 'esm-ua-parser-min': esmBundleForUAParser(), + umd: umdBundle, +}; + +// Collect all --config-* options and return the matching bundle configs +// Builds all bundles if no --config-* option given +// --config-cjs will build all three cjs-* bundles +// --config-umd will build only the umd bundle +// --config-umd --config-json will build both umd and the json-schema bundles +export default args => { + const patterns = Object.keys(args) + .filter(arg => arg.startsWith('config-')) + .map(arg => arg.replace(/config-/, '')); + + // default to matching all bundles + if (!patterns.length) patterns.push(/.*/); + + return Object.entries(bundles) + .filter(([name, config]) => patterns.some(pattern => name.match(pattern))) + .map(([name, config]) => config); +}; diff --git a/packages/optimizely-sdk/srcclr.yml b/srcclr.yml similarity index 100% rename from packages/optimizely-sdk/srcclr.yml rename to srcclr.yml diff --git a/tsconfig.json b/tsconfig.json index 000ff9c89..e70a7ce62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,64 +1,49 @@ { "compilerOptions": { - /* Basic Options */ - "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "target": "ES6", + "module": "ESNext", "lib": [ - "es2015", - "dom" - ] /* Specify library files to be included in the compilation. */, - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": true /* Generates corresponding '.d.ts' file. */, - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - "suppressImplicitAnyIndexErrors": true, - // "importHelpers": true /* Import emit helpers from 'tslib'. */, - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, - // "strictNullChecks": true /* Enable strict null checks. */, - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "ES6", + "DOM", + ], + "declaration": true, + "strict": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "moduleResolution": "node", + "esModuleInterop": true, + "baseUrl": "./", + "paths": { + "*": [ + "./typings/*" + ], + "error_message": [ + "./lib/message/error_message" + ], + "log_message": [ + "./lib/message/log_message" + ], + }, + "resolveJsonModule": true, + "allowJs": true, + "outDir": "./dist", + "sourceMap": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": false }, - "exclude": ["node_modules", "src/**/*.spec.ts"] + "exclude": [ + "./dist", + "./lib/**/*.tests.js", + "./lib/**/*.tests.ts", + "./lib/**/*.spec.ts", + "./lib/**/*.umdtests.js", + "./lib/tests", + "node_modules" + ], + "include": [ + "./lib/**/*.ts", + "./lib/**/*.js", + "./lib/modules/**/*.ts", + "./lib/modules/**/**/*.ts" + ] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 000000000..f61c713df --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [ + "vitest/jsdom" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "target": "ESNext" + }, + "exclude": [ + "./dist", + "./lib/**/*.tests.js", + "./lib/**/*.tests.ts", + "./lib/**/*.umdtests.js", + "node_modules" + ], + "include": [ + "./lib/**/*.ts", + "./lib/**/*.js", + "./lib/modules/**/*.ts", + "./lib/modules/**/**/*.ts", + "tests/**/*.ts", + "**/*.spec.ts" + ] +} diff --git a/typings/murmurhash.d.ts b/typings/murmurhash.d.ts new file mode 100644 index 000000000..5b097df23 --- /dev/null +++ b/typings/murmurhash.d.ts @@ -0,0 +1,10 @@ +declare module 'murmurhash' { + /** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @param key - ASCII only + * @param seed - (optional) positive integer + * @returns 32-bit positive integer hash + */ + function v3(key: string | Uint8Array, seed?: number): number; +} diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 000000000..1bce36eb0 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,35 @@ +/** + * Copyright 2024-2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import path from 'path'; +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + 'error_message': path.resolve(__dirname, './lib/message/error_message'), + 'log_message': path.resolve(__dirname, './lib/message/log_message'), + }, + }, + test: { + onConsoleLog: () => true, + environment: 'happy-dom', + include: ['**/*.spec.ts'], + typecheck: { + enabled: true, + tsconfig: 'tsconfig.spec.json', + }, + }, +});