├── .dockerignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── docker.yml │ ├── test.yml │ ├── unstable.yml │ └── update-license-year.yml ├── .gitignore ├── .jest ├── app.setup.js └── setEnvVars.js ├── .nvmrc ├── CHANGES.txt ├── CONTRIBUTORS-GUIDE.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── admin ├── __tests__ │ ├── healthcheck.test.js │ ├── machine.test.js │ ├── ping.test.js │ ├── stats.test.js │ ├── uptime.test.js │ └── version.test.js ├── admin.controller.js └── admin.router.js ├── app.js ├── babel.config.js ├── client ├── __tests__ │ ├── allTreatments.test.js │ ├── allTreatmentsWithConfig.test.js │ ├── track.test.js │ ├── treatment.test.js │ ├── treatmentWithConfig.test.js │ ├── treatments.test.js │ ├── treatmentsByFlagSets.test.js │ ├── treatmentsWithConfig.test.js │ └── treatmentsWithConfigByFlagSets.test.js ├── client.controller.js ├── client.router.js └── common.js ├── config ├── default.js └── test.js ├── environmentManager ├── __tests__ │ ├── clientReadines.test.js │ ├── environment.test.js │ ├── globalConfig.test.js │ ├── manager.test.js │ └── validation.test.js └── index.js ├── listener ├── __tests__ │ ├── ip-addresses.test.js │ ├── listener.test.js │ └── queue.test.js ├── index.js ├── manager.js ├── queue.js └── repeat.js ├── manager ├── __tests__ │ ├── split.test.js │ ├── splitNames.test.js │ └── splits.test.js ├── manager.controller.js └── manager.router.js ├── middleware └── authorization.js ├── openapi └── openapi.yaml ├── package-lock.json ├── package.json ├── sdk.js ├── server.js └── utils ├── constants.js ├── inputValidation ├── __tests__ │ ├── attributes.test.js │ ├── error.test.js │ ├── eventType.test.js │ ├── eventValue.test.js │ ├── key.test.js │ ├── keys.test.js │ ├── ok.test.js │ ├── properties.test.js │ ├── split.test.js │ ├── splits.test.js │ └── trafficType.test.js ├── attributes.js ├── eventType.js ├── flagSet.js ├── flagSets.js ├── key.js ├── keys.js ├── properties.js ├── split.js ├── splits.js ├── trafficType.js ├── value.js └── wrapper │ ├── error.js │ └── ok.js ├── lang ├── __tests__ │ ├── isFInite.test.js │ ├── isObject.test.js │ ├── isString.test.js │ └── uniq.test.js └── index.js ├── mocks ├── index.js └── splitchanges.since.-1.till.1602796638344.json ├── parserConfigs ├── __tests__ │ ├── index.test.js │ └── validators.test.js ├── index.js ├── split.yml └── validators.js ├── split1.yml ├── split2.yml ├── split3.yml ├── split4.yml ├── testWrapper └── index.js └── utils.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github/ 3 | node_modules/ 4 | npm-debug.log 5 | CHANGES.txt 6 | CONTRIBUTORS-GUIDE.md 7 | README.md 8 | Dockerfile 9 | LICENSE 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | 6 | "env": { 7 | "node": true, 8 | "es6": true, 9 | "jest": true 10 | }, 11 | 12 | "rules": { 13 | "indent": [2, 2, {"SwitchCase": 1}], 14 | "quotes": [1, "single", "avoid-escape"], 15 | "linebreak-style": [2, "unix"], 16 | "semi": [2, "always"], 17 | "no-underscore-dangle": 0, 18 | "eqeqeq": [2, "smart"], 19 | "no-unused-expressions": 0, 20 | "new-cap" : 0, 21 | "no-mixed-requires": 0, 22 | "camelcase": [2, {"properties": "never"}], 23 | "no-use-before-define": [2, "nofunc"], 24 | "comma-dangle": ["error", { 25 | "arrays": "never", 26 | "objects": "always-multiline", 27 | "imports": "never", 28 | "exports": "never", 29 | "functions": "never" 30 | }] 31 | }, 32 | 33 | "parserOptions": { 34 | "ecmaVersion": 2018, 35 | "sourceType": "module" 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @splitio/sdk 2 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | target-branch: "development" 10 | reviewers: 11 | - "splitio/sdk" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Split Evaluator 2 | 3 | ## What did you accomplish? 4 | 5 | ## How do we test the changes introduced in this PR? 6 | 7 | ## Extra Notes 8 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.run_number || github.event.pull_request.number }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | docker: 17 | name: Build Docker image 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup QEMU 24 | uses: docker/setup-qemu-action@v3 25 | with: 26 | platforms: amd64,arm64 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Login to Artifactory 32 | if: ${{ github.event_name == 'push' }} 33 | uses: docker/login-action@v3 34 | with: 35 | registry: splitio-docker-dev.jfrog.io 36 | username: ${{ secrets.ARTIFACTORY_DOCKER_USER }} 37 | password: ${{ secrets.ARTIFACTORY_DOCKER_PASS }} 38 | 39 | - name: Create build version 40 | run: echo "BUILD_VERSION=$(cat package.json | grep version | head -1 | awk '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]')" >> $GITHUB_ENV 41 | 42 | - name: Docker build 43 | uses: docker/build-push-action@v6 44 | with: 45 | context: . 46 | push: ${{ github.event_name == 'push' }} 47 | platforms: linux/amd64,linux/arm64 48 | tags: splitio-docker-dev.jfrog.io/${{ github.event.repository.name }}:${{ env.BUILD_VERSION}},splitio-docker-dev.jfrog.io/${{ github.event.repository.name }}:latest 49 | 50 | lacework: 51 | name: Scan Docker image 52 | if: ${{ github.event_name == 'pull_request' }} 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v4 57 | 58 | - name: Create build version 59 | run: echo "BUILD_VERSION=$(cat package.json | grep version | head -1 | awk '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]')" >> $GITHUB_ENV 60 | 61 | - name: Docker build 62 | uses: docker/build-push-action@v6 63 | with: 64 | context: . 65 | push: false 66 | tags: splitio-docker-dev.jfrog.io/${{ github.event.repository.name }}:${{ env.BUILD_VERSION}} 67 | build-args: | 68 | ARTIFACTORY_USER=${{ secrets.ARTIFACTORY_USER }} 69 | ARTIFACTORY_TOKEN=${{ secrets.ARTIFACTORY_TOKEN }} 70 | 71 | - name: Scan container using Lacework 72 | uses: lacework/lw-scanner-action@v1.4.1 73 | with: 74 | LW_ACCOUNT_NAME: ${{ secrets.LW_ACCOUNT_NAME }} 75 | LW_ACCESS_TOKEN: ${{ secrets.LW_ACCESS_TOKEN }} 76 | IMAGE_NAME: splitio-docker-dev.jfrog.io/${{ github.event.repository.name }} 77 | IMAGE_TAG: ${{ env.BUILD_VERSION}} 78 | SAVE_RESULTS_IN_LACEWORK: true 79 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches-ignore: 6 | - none 7 | 8 | jobs: 9 | build-and-test: 10 | name: Build and test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.18.0 20 | 21 | - run: npm ci 22 | - run: npm run lint 23 | - run: npm run test 24 | -------------------------------------------------------------------------------- /.github/workflows/unstable.yml: -------------------------------------------------------------------------------- 1 | name: unstable 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | push-docker-image: 10 | name: Build and Push Docker Image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Login to DockerHub 14 | uses: docker/login-action@v3 15 | with: 16 | registry: splitio-docker-dev.jfrog.io 17 | username: ${{ secrets.ARTIFACTORY_DOCKER_USER }} 18 | password: ${{ secrets.ARTIFACTORY_DOCKER_PASS }} 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup QEMU 24 | uses: docker/setup-qemu-action@v3 25 | with: 26 | platforms: amd64,arm64 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Get short hash 32 | run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV 33 | 34 | - name: Docker Build 35 | uses: docker/build-push-action@v6 36 | with: 37 | context: . 38 | push: true 39 | platforms: linux/amd64,linux/arm64 40 | tags: splitio-docker-dev.jfrog.io/${{ github.event.repository.name }}:${{ env.SHORT_SHA}} 41 | -------------------------------------------------------------------------------- /.github/workflows/update-license-year.yml: -------------------------------------------------------------------------------- 1 | name: Update License Year 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 1 1 *" # 03:00 AM on January 1 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set Current year 21 | run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV" 22 | 23 | - name: Set Previous Year 24 | run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV" 25 | 26 | - name: Update LICENSE 27 | uses: jacobtomlinson/gha-find-replace@v3 28 | with: 29 | find: ${{ env.PREVIOUS }} 30 | replace: ${{ env.CURRENT }} 31 | include: "LICENSE" 32 | regex: false 33 | 34 | - name: Commit files 35 | run: | 36 | git config user.name 'github-actions[bot]' 37 | git config user.email 'github-actions[bot]@users.noreply.github.com' 38 | git commit -m "Updated License Year" -a 39 | 40 | - name: Create Pull Request 41 | uses: peter-evans/create-pull-request@v7 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | title: Update License Year 45 | branch: update-license 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_STORE 3 | node_modules 4 | log.txt 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.jest/app.setup.js: -------------------------------------------------------------------------------- 1 | const { gracefulShutDown } = require('../utils/testWrapper/index'); 2 | 3 | afterAll(async () => { 4 | await gracefulShutDown(); 5 | }) 6 | -------------------------------------------------------------------------------- /.jest/setEnvVars.js: -------------------------------------------------------------------------------- 1 | // Environments for testing 2 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = `[ 3 | {"API_KEY":"localhost","AUTH_TOKEN":"test"}, 4 | {"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"}, 5 | {"API_KEY":"apikey2","AUTH_TOKEN":"key_red"}, 6 | {"API_KEY":"apikey3","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_green"}, 7 | {"API_KEY":"apikey4","AUTH_TOKEN":"key_purple","FLAG_SET_FILTER":"set_purple"}, 8 | {"API_KEY":"apikey5","AUTH_TOKEN":"key_pink","FLAG_SET_FILTER":"set_green,set_purple"} 9 | ]`; 10 | 11 | // Before all tests, sdk module is mocked to create a wrapper where a different yaml file is assigned to each environment 12 | // sdk factory mock to set a different yaml for each apikey and localhost mode 13 | jest.mock('../sdk', () => ({ 14 | getSplitFactory: jest.fn((settings) => { 15 | const { __dirname } = require('../utils/utils'); 16 | const path = require('path'); 17 | const { apiKeyMocksMap } = require('../utils/mocks') 18 | 19 | // Clients are configured in localhost mode if there is a features file maped to the authorizationKey value in mocksMap 20 | 21 | let features = ''; 22 | let authorizationKey = settings.core.authorizationKey; 23 | let error = { te: { 401: 1 } }; 24 | 25 | const mock = apiKeyMocksMap[settings.core.authorizationKey] 26 | 27 | // if authorizationKey has a feature file maped in mocksMap 28 | if (mock) { 29 | // Set feature file 30 | features = path.join(__dirname, mock.splitUrl); 31 | // Set mode to localhost 32 | authorizationKey = 'localhost' 33 | // Set mockError 34 | error = {} 35 | }; 36 | 37 | const configForMock = { 38 | ...settings, 39 | core: { 40 | ...settings.core, 41 | authorizationKey: authorizationKey, 42 | }, 43 | urls: { 44 | sdk: 'https://sdk.test.io/api', 45 | events: 'https://events.test.io/api', 46 | auth: 'https://auth.test.io/api', 47 | streaming: 'https://streaming.test.io', 48 | telemetry: 'https://telemetry.test.io/api', 49 | }, 50 | startup: { 51 | readyTimeout: 1, 52 | }, 53 | features: features, 54 | scheduler: { 55 | ...settings.scheduler, 56 | featuresRefreshRate: 1, 57 | segmentsRefreshRate: 1, 58 | impressionsRefreshRate: 30000 59 | }, 60 | streamingEnabled: false 61 | }; 62 | 63 | let sdk = jest.requireActual('../sdk'); 64 | const { factory, impressionsMode } = sdk.getSplitFactory(configForMock); 65 | 66 | const mockedTelemetry = { 67 | splits: { 68 | getSplitNames: () => mock ? mock.splitNames : [] 69 | }, 70 | segments: { 71 | getRegisteredSegments: () => mock ? mock.segments : [] 72 | }, 73 | getLastSynchronization: () => mock ? mock.lastSynchronization : {}, 74 | getTimeUntilReady: () => mock ? mock.timeUntilReady : 0, 75 | httpErrors: error, 76 | } 77 | 78 | return { factory, telemetry: mockedTelemetry, impressionsMode }; 79 | }), 80 | })); -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.13.1 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Split Evaluator 2 | 3 | Split Evaluator is an open source project and we welcome feedback and contribution. Find below information about how to build the project with your changes, how to run the tests and how to send the PR. 4 | 5 | ## Development 6 | 7 | ### Development process 8 | 9 | 1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch. 10 | 2. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like "fix bug". 11 | 3. Make sure to add tests for both positive and negative cases. 12 | 4. Run the linter script of the project and fix any issues you find. 13 | 5. Run the build script and make sure it runs with no errors. 14 | 6. Run all tests and make sure there are no failures. 15 | 7. `git push` your changes to GitHub within your topic branch. 16 | 8. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository. 17 | 9. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project. 18 | 10. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`. 19 | 11. Keep an eye out for any feedback or comments from Split's SDK team. 20 | 21 | ### Building the Split Evaluator 22 | #### Usage with NodeJs 23 | If you're just trying to run the Node app, run `npm install` on the root of the project. No extra build steps needed. 24 | 25 | #### Docker 26 | If you want to build a Docker Image, you need to execute the following command at root folder: 27 | `docker build -t splitsoftware/split-evaluator:X.X.X .` 28 | 29 | ### Running tests 30 | You can run `npm run test` for running all the unit tests placed in the project. 31 | 32 | ### Linting and other useful checks 33 | If you want to check linting, you can run `npm run lint`. 34 | 35 | # Contact 36 | If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io. 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder stage 2 | FROM node:20.13.1-alpine3.20 AS builder 3 | 4 | WORKDIR /usr/src/split-evaluator 5 | 6 | COPY package.json package-lock.json ./ 7 | 8 | RUN npm install --only=production 9 | 10 | # Runner stage 11 | FROM node:20.13.1-alpine3.20 AS runner 12 | 13 | WORKDIR /usr/src/split-evaluator 14 | 15 | COPY --from=builder /usr/src/split-evaluator/node_modules ./node_modules 16 | 17 | COPY . . 18 | 19 | EXPOSE 7548 20 | 21 | ENV SPLIT_EVALUATOR_SERVER_PORT=7548 22 | 23 | ENTRYPOINT ["npm", "start"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2024 Split Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Setup defaults 2 | MAKE ?= make 3 | DOCKER ?= docker 4 | PLATFORM ?= linux/arm64/v8,linux/amd64 5 | BUILDER ?= container 6 | 7 | version := $(shell cat package.json | grep '"version": "' | sed 's/ "version": "//' | tr -d '",') 8 | 9 | # Help target borrowed from: https://docs.cloudposse.com/reference/best-practices/make-best-practices/ 10 | ## This help screen 11 | help: 12 | @printf "Available targets:\n\n" 13 | @awk '/^[a-zA-Z\-\_0-9%:\\]+/ { \ 14 | helpMessage = match(lastLine, /^## (.*)/); \ 15 | if (helpMessage) { \ 16 | helpCommand = $$1; \ 17 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 18 | gsub("\\\\", "", helpCommand); \ 19 | gsub(":+$$", "", helpCommand); \ 20 | printf " \x1b[32;01m%-35s\x1b[0m %s\n", helpCommand, helpMessage; \ 21 | } \ 22 | } \ 23 | { lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u 24 | @printf "\n" 25 | 26 | ## Build release-ready docker images with proper tags and output push commands in stdout 27 | images_release_multi_load: # entrypoints 28 | @echo "make sure you have buildx configured 'docker buildx ls', if not 'docker buildx create --name container --driver=docker-container'" 29 | $(DOCKER) buildx build \ 30 | -t splitsoftware/split-evaluator:latest -t splitsoftware/split-evaluator:$(version) \ 31 | --platform $(PLATFORM) \ 32 | --builder $(BUILDER) \ 33 | --load . 34 | @echo "Images created. Make sure everything works ok, and then run the following commands to push them." 35 | @echo "$(DOCKER) push splitsoftware/split-evaluator:$(version)" 36 | @echo "$(DOCKER) push splitsoftware/split-evaluator:latest" 37 | 38 | platform_str = $(if $(PLATFORM),--platform $(PLATFORM),) 39 | builder_str = $(if $(BUILDER),--builder $(BUILDER),) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Split Evaluator 2 | [![build workflow](https://github.com/splitio/split-evaluator/actions/workflows/ci.yml/badge.svg)](https://github.com/splitio/split-evaluator/actions) 3 | [![Documentation](https://img.shields.io/badge/split_evaluator-documentation-informational)](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) 4 | 5 | ## Overview 6 | This services exposes a set of APIs to produce server side evaluation of flags by wrapping a NodeJS SDK. 7 | 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware) 9 | 10 | ## Compatibility 11 | Split Evaluator supports Node version 8 or higher. 12 | 13 | ## Getting started 14 | Below is a simple example that describes the instantiation of Split Evaluator: 15 | 16 | ### Usage via NodeJs 17 | 1. Install npm packages via `npm install` 18 | 2. Then, execute `SPLIT_EVALUATOR_API_KEY= SPLIT_EVALUATOR_AUTH_TOKEN= SPLIT_EVALUATOR_SERVER_PORT=7548 SPLIT_EVALUATOR_LOG_LEVEL=debug npm start` 19 | 20 | ### Docker 21 | 1. You can pull the Docker image from [Docker Hub](https://hub.docker.com/r/splitsoftware/split-evaluator) and run it into your container environment. 22 | 23 | ```shell 24 | docker pull splitsoftware/split-evaluator:latest 25 | ``` 26 | 27 | 2. Run the image: 28 | 29 | ```shell 30 | docker run --rm --name split-evaluator \ 31 | -p 7548:7548 \ 32 | -e SPLIT_EVALUATOR_API_KEY= \ 33 | -e SPLIT_EVALUATOR_AUTH_TOKEN= \ 34 | splitsoftware/split-evaluator 35 | ``` 36 | 37 | Please refer to [our official docs](https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator) to learn about all the functionality provided by Split Evaluator and the configuration options available for tailoring it to your current application setup. 38 | 39 | ## Submitting issues 40 | The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/split-evaluator/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner. 41 | 42 | ## Contributing 43 | Please see [Contributors Guide](CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR). 44 | 45 | ## License 46 | Licensed under the Apache License, Version 2.0. See: [Apache License](http://www.apache.org/licenses/). 47 | 48 | ## About Split 49 | 50 | Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy features as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new features will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery. 51 | 52 | To learn more about Split, contact hello@split.io, or get started with feature flags for free at https://www.split.io/signup. 53 | 54 | Split has built and maintains SDKs for: 55 | 56 | * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) 57 | * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) 58 | * Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) 59 | * Flutter [Github](https://github.com/splitio/flutter-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) 60 | * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) 61 | * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) 62 | * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) 63 | * JavaScript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) 64 | * JavaScript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) 65 | * Node.js [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) 66 | * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) 67 | * PHP thin-client [Github](https://github.com/splitio/php-thin-client) [Docs](https://help.split.io/hc/en-us/articles/18305128673933-PHP-Thin-Client-SDK) 68 | * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) 69 | * React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) 70 | * React Native [Github](https://github.com/splitio/react-native-client) [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) 71 | * Redux [Github](https://github.com/splitio/redux-client) [Docs](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK) 72 | * Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) 73 | 74 | For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20). 75 | 76 | **Learn more about Split:** 77 | 78 | Visit [split.io/product](https://www.split.io/product) for an overview of Split, or visit our documentation at [help.split.io](https://help.split.io) for more detailed information. 79 | -------------------------------------------------------------------------------- /admin/__tests__/healthcheck.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | const { expectError } = require('../../utils/testWrapper/index'); 4 | 5 | describe('healthcheck', () => { 6 | // Testing authorization 7 | test('should be 401 if auth is not passed', async () => { 8 | const response = await request(app) 9 | .get('/admin/healthcheck'); 10 | expectError(response, 401, 'Unauthorized'); 11 | }); 12 | 13 | test('should be 401 if auth does not match', async () => { 14 | const response = await request(app) 15 | .get('/admin/healthcheck') 16 | .set('Authorization', 'invalid'); 17 | expectError(response, 401, 'Unauthorized'); 18 | }); 19 | 20 | test('should be 200', async () => { 21 | const response = await request(app) 22 | .get('/admin/healthcheck') 23 | .set('Authorization', 'test'); 24 | expect(response.statusCode).toEqual(200); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /admin/__tests__/machine.test.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const ip = require('@splitsoftware/splitio/cjs/utils/ip'); 3 | const request = require('supertest'); 4 | const app = require('../../app'); 5 | const { expectError } = require('../../utils/testWrapper/index'); 6 | 7 | describe('machine', () => { 8 | // Testing authorization 9 | test('should be 401 if auth is not passed', async () => { 10 | const response = await request(app) 11 | .get('/admin/machine'); 12 | expectError(response, 401, 'Unauthorized'); 13 | }); 14 | 15 | test('should be 401 if auth does not match', async () => { 16 | const response = await request(app) 17 | .get('/admin/machine') 18 | .set('Authorization', 'invalid'); 19 | expectError(response, 401, 'Unauthorized'); 20 | }); 21 | 22 | test('should be 200', async () => { 23 | const response = await request(app) 24 | .get('/admin/machine') 25 | .set('Authorization', 'test'); 26 | expect(response.statusCode).toEqual(200); 27 | expect(response.body).toHaveProperty('ip', ip.address()); 28 | expect(response.body).toHaveProperty('name', os.hostname()); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /admin/__tests__/ping.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | const { expectError } = require('../../utils/testWrapper/index'); 4 | 5 | describe('ping', () => { 6 | // Testing authorization 7 | test('should be 401 if auth is not passed', async () => { 8 | const response = await request(app) 9 | .get('/admin/ping'); 10 | expectError(response, 401, 'Unauthorized'); 11 | }); 12 | 13 | test('should be 401 if auth does not match', async () => { 14 | const response = await request(app) 15 | .get('/admin/ping') 16 | .set('Authorization', 'invalid'); 17 | expectError(response, 401, 'Unauthorized'); 18 | }); 19 | 20 | test('should be 200', async () => { 21 | const response = await request(app) 22 | .get('/admin/ping') 23 | .set('Authorization', 'test'); 24 | expect(response.statusCode).toBe(200); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /admin/__tests__/stats.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { expectError } = require('../../utils/testWrapper/index'); 3 | const utils = require('../../utils/utils'); 4 | const { apiKeyMocksMap } = require('../../utils/mocks'); 5 | 6 | jest.mock('node-fetch', () => { 7 | return jest.fn().mockImplementation(() => { 8 | return Promise.reject({ response: { status: 404, response: 'response' } }); 9 | }); 10 | }); 11 | describe('stats', () => { 12 | 13 | beforeEach(() => { 14 | jest.resetModules(); 15 | jest.clearAllMocks(); 16 | }); 17 | 18 | afterAll(() => { 19 | // Unmock fetch 20 | jest.unmock('node-fetch'); 21 | }); 22 | 23 | describe('stats', () => { 24 | 25 | 26 | const environments = JSON.parse(process.env.SPLIT_EVALUATOR_ENVIRONMENTS); 27 | 28 | // Testing authorization 29 | test('should be 401 if auth is not passed', async () => { 30 | const app = require('../../app'); 31 | const response = await request(app) 32 | .get('/admin/stats'); 33 | expectError(response, 401, 'Unauthorized'); 34 | }); 35 | 36 | test('should be 401 if auth does not match', async () => { 37 | const app = require('../../app'); 38 | const response = await request(app) 39 | .get('/admin/stats') 40 | .set('Authorization', 'invalid'); 41 | expectError(response, 401, 'Unauthorized'); 42 | }); 43 | 44 | test('uptime and healthcheck', async () => { 45 | const app = require('../../app'); 46 | const version = utils.getVersion(); 47 | const environmentManager = require('../../environmentManager').getInstance(); 48 | const parts = environmentManager.getVersion().split('-'); 49 | const sdkLanguage = parts[0]; 50 | const sdkVersion = parts.slice(1).join('-'); 51 | 52 | const response = await request(app) 53 | .get('/admin/stats') 54 | .set('Authorization', 'key_blue'); 55 | expect(response.statusCode).toEqual(200); 56 | const stats = response.body; 57 | expect(stats.uptime).toEqual(utils.uptime()); 58 | expect(stats.healthcheck).toEqual({ 59 | version: version, 60 | sdk: sdkLanguage, 61 | sdkVersion: sdkVersion, 62 | }); 63 | }); 64 | 65 | test('ready environment stats', async () => { 66 | const environmentManager = require('../../environmentManager').getInstance(); 67 | const app = require('../../app'); 68 | const response = await request(app) 69 | .get('/admin/stats') 70 | .set('Authorization', 'test'); 71 | expect(response.statusCode).toEqual(200); 72 | const stats = response.body; 73 | environments.forEach(environment => { 74 | const authToken = environment.AUTH_TOKEN; 75 | const apiKey = environment.API_KEY; 76 | const mock = apiKeyMocksMap[apiKey]; 77 | if (!mock) return; 78 | expect(stats.environments[utils.obfuscate(authToken)]).toEqual({ 79 | splitCount: mock.splitNames.length, 80 | segmentCount: mock.segments.length, 81 | ready: true, 82 | timeUntilReady: mock.timeUntilReady, 83 | lastSynchronization: environmentManager._reword(mock.lastSynchronization), 84 | httpErrors: environmentManager._reword(mock.httpErrors), 85 | impressionsMode: 'OPTIMIZED', 86 | }); 87 | }); 88 | }); 89 | 90 | test('stats for two environments where one is timed out', async () => { 91 | // Environment configurations 92 | const environmentsConfig = [ 93 | { API_KEY: 'test1', AUTH_TOKEN: 'timedout' }, 94 | { API_KEY: 'apikey1', AUTH_TOKEN: 'key_blue' } 95 | ]; 96 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 97 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = JSON.stringify(environmentsConfig); 98 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ 99 | sync: { 100 | impressionsMode: 'NONE', 101 | }, 102 | }); 103 | const app = require('../../app'); 104 | const environmentManager = require('../../environmentManager').getInstance(); 105 | 106 | let response = await request(app) 107 | .get('/admin/stats') 108 | .set('Authorization', 'key_blue'); 109 | expect(response.statusCode).toEqual(200); 110 | let stats = response.body; 111 | environmentsConfig.forEach(envConfig => { 112 | const authToken = envConfig.AUTH_TOKEN; 113 | const apiKey = envConfig.API_KEY; 114 | let mock = apiKeyMocksMap[apiKey]; 115 | // if there is not a mock it works as timed out 116 | if (!mock) { 117 | mock = { 118 | splitNames: [], 119 | segments: [], 120 | timeUntilReady: 0, 121 | lastSynchronization: {}, 122 | httpErrors: { te: { 401: 1 } }, 123 | }; 124 | } 125 | const environment = environmentManager.getEnvironment(authToken); 126 | expect(stats.environments[utils.obfuscate(authToken)]).toEqual({ 127 | splitCount: mock.splitNames.length, 128 | segmentCount: mock.segments.length, 129 | ready: environment.isClientReady, 130 | timeUntilReady: mock.timeUntilReady, 131 | lastSynchronization: environmentManager._reword(mock.lastSynchronization), 132 | httpErrors: environmentManager._reword(mock.httpErrors), 133 | impressionsMode: 'NONE', 134 | lastEvaluation: environment.lastEvaluation, 135 | }); 136 | 137 | }); 138 | response = await request(app) 139 | .get('/client/get-treatment?key=test&split-name=testing_split_blue') 140 | .set('Authorization', 'key_blue'); 141 | 142 | expect(response.statusCode).toEqual(200); 143 | response = await request(app) 144 | .get('/admin/stats') 145 | .set('Authorization', 'key_blue'); 146 | expect(response.statusCode).toEqual(200); 147 | stats = response.body; 148 | const lastEvaluation = stats.environments[utils.obfuscate('key_blue')].lastEvaluation; 149 | expect(lastEvaluation).not.toBe(undefined); 150 | expect(lastEvaluation).toBe(environmentManager.getEnvironment('key_blue').lastEvaluation); 151 | 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /admin/__tests__/uptime.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | const { expectError } = require('../../utils/testWrapper/index'); 4 | 5 | describe('uptime', () => { 6 | // Testing authorization 7 | test('should be 401 if auth is not passed', async () => { 8 | const response = await request(app) 9 | .get('/admin/uptime'); 10 | expectError(response, 401, 'Unauthorized'); 11 | }); 12 | 13 | test('should be 401 if auth does not match', async () => { 14 | const response = await request(app) 15 | .get('/admin/uptime') 16 | .set('Authorization', 'invalid'); 17 | expectError(response, 401, 'Unauthorized'); 18 | }); 19 | 20 | test('should be 200', async () => { 21 | const response = await request(app) 22 | .get('/admin/uptime') 23 | .set('Authorization', 'test'); 24 | expect(response.statusCode).toBe(200); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /admin/__tests__/version.test.js: -------------------------------------------------------------------------------- 1 | const utils = require('../../utils/utils'); 2 | const environmentManager = require('../../environmentManager').getInstance(); 3 | 4 | const request = require('supertest'); 5 | const app = require('../../app'); 6 | const { expectError } = require('../../utils/testWrapper/index'); 7 | 8 | describe('version', () => { 9 | // Testing authorization 10 | test('should be 401 if auth is not passed', async () => { 11 | const response = await request(app) 12 | .get('/admin/version'); 13 | expectError(response, 401, 'Unauthorized'); 14 | }); 15 | 16 | test('should be 401 if auth does not match', async () => { 17 | const response = await request(app) 18 | .get('/admin/version') 19 | .set('Authorization', 'invalid'); 20 | expectError(response, 401, 'Unauthorized'); 21 | }); 22 | 23 | test('should be 200', async () => { 24 | const response = await request(app) 25 | .get('/admin/version') 26 | .set('Authorization', 'test'); 27 | const version = utils.getVersion(); 28 | const parts = environmentManager.getVersion().split('-'); 29 | const sdkLanguage = parts[0]; 30 | const sdkVersion = parts.slice(1).join('-'); 31 | expect(response.statusCode).toEqual(200); 32 | expect(response.body).toHaveProperty('sdk', sdkLanguage); 33 | expect(response.body).toHaveProperty('sdkVersion', sdkVersion); 34 | expect(response.body).toHaveProperty('version', version); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /admin/admin.controller.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const ip = require('@splitsoftware/splitio/cjs/utils/ip'); 3 | 4 | const utils = require('../utils/utils'); 5 | const environmentManager = require('../environmentManager').getInstance(); 6 | 7 | /** 8 | * ping pings server 9 | * @param {*} req 10 | * @param {*} res 11 | */ 12 | const ping = (req, res) => { 13 | console.log('pong'); 14 | res.status(200).send('pong'); 15 | }; 16 | 17 | /** 18 | * healthcheck checks if SDK and environtment is healthy or not. 19 | * 200 - Everything is OK. Ready to evaluate. 20 | * 500 - Something is wrong or not ready yet. Not able to perform evaluations. 21 | * @param {*} req 22 | * @param {*} res 23 | */ 24 | const healthcheck = (req, res) => { 25 | console.log('Running health check.'); 26 | let status = 500; 27 | let msg = 'Split evaluator engine is not evaluating traffic properly.'; 28 | 29 | if (environmentManager.isReady()) { 30 | status = 200; 31 | msg = 'Split Evaluator working as expected.'; 32 | } 33 | 34 | console.log('Health check status: ' + status + ' - ' + msg); 35 | res.status(status).send(msg); 36 | }; 37 | 38 | const getVersion = () => { 39 | const version = utils.getVersion(); 40 | const parts = environmentManager.getVersion().split('-'); 41 | const sdkLanguage = parts[0]; 42 | const sdkVersion = parts.slice(1).join('-'); 43 | return { 44 | version, 45 | sdk: sdkLanguage, 46 | sdkVersion, 47 | }; 48 | }; 49 | 50 | /** 51 | * version returns the current version of Split Evaluator 52 | * @param {*} req 53 | * @param {*} res 54 | */ 55 | const version = (req, res) => { 56 | res.send(getVersion()); 57 | }; 58 | 59 | /** 60 | * machine returns the machine instance name and machine ip 61 | * @param {*} req 62 | * @param {*} res 63 | */ 64 | const machine = (req, res) => { 65 | console.log('Getting machine information.'); 66 | let address; let hostname; 67 | 68 | try { 69 | address = ip.address(); 70 | hostname = os.hostname(); 71 | } catch(e) { 72 | address = hostname = 'unavailable'; 73 | } 74 | 75 | res.send({ 76 | ip: address, 77 | name: hostname, 78 | }); 79 | }; 80 | 81 | /** 82 | * uptime returns the uptime of the server 83 | * @param {*} req 84 | * @param {*} res 85 | */ 86 | const uptime = (req, res) => { 87 | const uptime = utils.uptime(); 88 | console.log('Getting uptime: ' + uptime); 89 | res.send('' + uptime); 90 | }; 91 | 92 | /** 93 | * stats Return current status for evaluator and environments. 94 | * Evaluator stats in response: 95 | * - HealthCheck (Version, sdk and sdkVersion) 96 | * - UpTime 97 | * Stats for each environment: 98 | * - readiness 99 | * - splits 100 | * - segments 101 | * - impressionsMode 102 | * - last Evaluation 103 | * 104 | * 200 - Everything is OK. Evaluator returns stats. 105 | * 500 - Something is wrong or not ready yet. Not able to get stats. 106 | * @param {*} req 107 | * @param {*} res 108 | */ 109 | const stats = (req, res) => { 110 | console.log('Running stats.'); 111 | let stats = { 112 | uptime: utils.uptime(), 113 | environments: {}, 114 | }; 115 | 116 | stats.healthcheck = getVersion(); 117 | if (!environmentManager.isReady()) { 118 | res.status(500).send('Split evaluator engine is not evaluating traffic properly.'); 119 | } 120 | 121 | const authTokens = environmentManager.getAuthTokens(); 122 | authTokens.forEach((authToken) => { 123 | stats.environments[utils.obfuscate(authToken)] = environmentManager.getTelemetry(authToken); 124 | }); 125 | 126 | res.status(200).send(stats); 127 | }; 128 | 129 | module.exports = { 130 | ping, 131 | healthcheck, 132 | version, 133 | machine, 134 | uptime, 135 | stats, 136 | }; 137 | -------------------------------------------------------------------------------- /admin/admin.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const adminController = require('./admin.controller'); 4 | 5 | router.get('/ping', adminController.ping); 6 | router.get('/healthcheck', adminController.healthcheck); 7 | router.get('/version', adminController.version); 8 | router.get('/machine', adminController.machine); 9 | router.get('/uptime', adminController.uptime); 10 | router.get('/stats', adminController.stats); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const swaggerUI = require('swagger-ui-express'); 4 | const YAML = require('js-yaml'); 5 | const fs = require('fs'); 6 | const app = express(); 7 | const { validUrl } = require('./utils/parserConfigs/validators'); 8 | const environmentManager = require('./environmentManager').getInstance(); 9 | 10 | // Middlewares 11 | const authorization = require('./middleware/authorization'); 12 | 13 | // Routes 14 | const clientRouter = require('./client/client.router'); 15 | const managerRouter = require('./manager/manager.router'); 16 | const adminRouter = require('./admin/admin.router'); 17 | 18 | // Utils 19 | const utils = require('./utils/utils'); 20 | 21 | app.use(morgan('tiny')); 22 | 23 | // OPENAPI 3.0 Definition 24 | // Grabs yaml 25 | const openApiDefinition = YAML.load(fs.readFileSync('./openapi/openapi.yaml').toString()); 26 | // Informs warn and remove security tag 27 | if (!environmentManager.requireAuth) { 28 | delete openApiDefinition.security; 29 | delete openApiDefinition.components.securitySchemes; 30 | console[console.warn ? 'warn' : 'log']('External API key not provided. If you want a security filter use the SPLIT_EVALUATOR_AUTH_TOKEN environment variable as explained in our documentation.'); 31 | } 32 | // Updates version to current one 33 | openApiDefinition.info.version = utils.getVersion(); 34 | // Puts server url and port 35 | 36 | // SWAGGER URL 37 | const swaggerUrl = process.env.SPLIT_EVALUATOR_SWAGGER_URL ? validUrl('SPLIT_EVALUATOR_SWAGGER_URL') : `http://localhost:${process.env.SPLIT_EVALUATOR_SERVER_PORT || 7548}`; 38 | openApiDefinition.servers = [{url: swaggerUrl}]; 39 | 40 | app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(openApiDefinition)); 41 | 42 | // Auth middleware 43 | app.use(authorization); 44 | // We mount our routers. 45 | app.use('/client', clientRouter); 46 | app.use('/manager', managerRouter); 47 | app.use('/admin', adminRouter); 48 | app.get('/favicon.ico', (req, res) => res.status(204)); 49 | 50 | //Route not found -- Set 404 51 | app.get('*', function (req, res) { 52 | console.log('Wrong endpoint called.'); 53 | res.json({ 54 | 'route': 'Sorry this page does not exist!', 55 | }); 56 | }); 57 | 58 | module.exports = app; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | } 10 | ] 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /client/__tests__/track.test.js: -------------------------------------------------------------------------------- 1 | process.env.SPLIT_EVALUATOR_AUTH_TOKEN = 'test'; 2 | process.env.SPLIT_EVALUATOR_API_KEY = 'localhost'; 3 | 4 | const request = require('supertest'); 5 | const app = require('../../app'); 6 | const { expectError, expectErrorContaining, getLongKey } = require('../../utils/testWrapper'); 7 | 8 | describe('track', () => { 9 | // Testing authorization 10 | test('should be 401 if auth is not passed', async () => { 11 | const response = await request(app) 12 | .get('/client/track?key=test&event-type=my-event&traffic-type=my-traffic&value=1'); 13 | expectError(response, 401, 'Unauthorized'); 14 | }); 15 | 16 | test('should be 401 if auth does not match', async () => { 17 | const response = await request(app) 18 | .get('/client/track?key=test&event-type=my-event&traffic-type=my-traffic&value=1') 19 | .set('Authorization', 'invalid'); 20 | expectError(response, 401, 'Unauthorized'); 21 | }); 22 | 23 | // Testing Input Validation. 24 | // The following tests are going to check null parameters, wrong types or lengths. 25 | test('should be 400 if key is not passed', async () => { 26 | const expected = [ 27 | 'you passed a null or undefined key, key must be a non-empty string.' 28 | ]; 29 | const response = await request(app) 30 | .get('/client/track?event-type=my-event&traffic-type=my-traffic&value=1') 31 | .set('Authorization', 'test'); 32 | expectErrorContaining(response, 400, expected); 33 | }); 34 | 35 | test('should be 400 if key is empty', async () => { 36 | const expected = [ 37 | 'you passed an empty string, key must be a non-empty string.' 38 | ]; 39 | const response = await request(app) 40 | .get('/client/track?key=&event-type=my-event&traffic-type=my-traffic&value=1') 41 | .set('Authorization', 'test'); 42 | expectErrorContaining(response, 400, expected); 43 | }); 44 | 45 | test('should be 400 if key is empty trimmed', async () => { 46 | const expected = [ 47 | 'you passed an empty string, key must be a non-empty string.' 48 | ]; 49 | const response = await request(app) 50 | .get('/client/track?key= &event-type=my-event&traffic-type=my-traffic&value=1') 51 | .set('Authorization', 'test'); 52 | expectErrorContaining(response, 400, expected); 53 | }); 54 | 55 | test('should be 400 if key is too long', async () => { 56 | const expected = [ 57 | 'key too long, key must be 250 characters or less.' 58 | ]; 59 | const key = getLongKey(); 60 | const response = await request(app) 61 | .get(`/client/track?key=${key}&event-type=my-event&traffic-type=my-traffic&value=1`) 62 | .set('Authorization', 'test'); 63 | expectErrorContaining(response, 400, expected); 64 | }); 65 | 66 | test('should be 400 if event-type is not passed', async () => { 67 | const expected = [ 68 | 'you passed a null or undefined event-type, event-type must be a non-empty string.' 69 | ]; 70 | const response = await request(app) 71 | .get('/client/track?key=my-key&traffic-type=my-traffic&value=1') 72 | .set('Authorization', 'test'); 73 | expectErrorContaining(response, 400, expected); 74 | }); 75 | 76 | test('should be 400 if event-type is empty', async () => { 77 | const expected = [ 78 | 'you passed an empty event-type, event-type must be a non-empty string.' 79 | ]; 80 | const response = await request(app) 81 | .get('/client/track?key=my-key&event-type=&traffic-type=my-traffic&value=1') 82 | .set('Authorization', 'test'); 83 | expectErrorContaining(response, 400, expected); 84 | }); 85 | 86 | test('should be 400 if event-type not accomplish regex', async () => { 87 | const expected = [ 88 | 'you passed "@!test", event-type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.' 89 | ]; 90 | const response = await request(app) 91 | .get('/client/track?key=my-key&event-type=@!test&traffic-type=my-traffic&value=1') 92 | .set('Authorization', 'test'); 93 | expectErrorContaining(response, 400, expected); 94 | }); 95 | 96 | test('should be 400 if traffic-type is not passed', async () => { 97 | const expected = [ 98 | 'you passed a null or undefined traffic-type, traffic-type must be a non-empty string.' 99 | ]; 100 | const response = await request(app) 101 | .get('/client/track?key=my-key&event-type=my-event&value=1') 102 | .set('Authorization', 'test'); 103 | expectErrorContaining(response, 400, expected); 104 | }); 105 | 106 | test('should be 400 if traffic-type is empty', async () => { 107 | const expected = [ 108 | 'you passed an empty traffic-type, traffic-type must be a non-empty string.' 109 | ]; 110 | const response = await request(app) 111 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=&value=1') 112 | .set('Authorization', 'test'); 113 | expectErrorContaining(response, 400, expected); 114 | }); 115 | 116 | test('should be 400 if value is empty', async () => { 117 | const expected = [ 118 | 'value must be null or number.' 119 | ]; 120 | const response = await request(app) 121 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&value=') 122 | .set('Authorization', 'test'); 123 | expectErrorContaining(response, 400, expected); 124 | }); 125 | 126 | test('should be 400 if value is empty', async () => { 127 | const expected = [ 128 | 'value must be null or number.' 129 | ]; 130 | const response = await request(app) 131 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&value=') 132 | .set('Authorization', 'test'); 133 | expectErrorContaining(response, 400, expected); 134 | }); 135 | 136 | test('should be 400 if value is NaN', async () => { 137 | const expected = [ 138 | 'value must be null or number.' 139 | ]; 140 | const response = await request(app) 141 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&value=not-number') 142 | .set('Authorization', 'test'); 143 | expectErrorContaining(response, 400, expected); 144 | }); 145 | 146 | test('should be 400 if properties is invalid', async () => { 147 | const expected = [ 148 | 'properties must be a plain object.' 149 | ]; 150 | const response = await request(app) 151 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&value=1&properties=lalala') 152 | .set('Authorization', 'test'); 153 | expectErrorContaining(response, 400, expected); 154 | }); 155 | 156 | test('should be 400 if there are multiple errors in every input', async () => { 157 | const expected = [ 158 | 'key too long, key must be 250 characters or less.', 159 | 'you passed "@!test", event-type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.', 160 | 'you passed an empty traffic-type, traffic-type must be a non-empty string.', 161 | 'properties must be a plain object.', 162 | 'value must be null or number.' 163 | ]; 164 | const key = getLongKey(); 165 | const response = await request(app) 166 | .get(`/client/track?key=${key}&event-type=@!test&traffic-type=&value=invalid&properties=invalid`) 167 | .set('Authorization', 'test'); 168 | expectErrorContaining(response, 400, expected); 169 | }); 170 | 171 | test('should be 200 if properties is null', async () => { 172 | const response = await request(app) 173 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&value=1') 174 | .set('Authorization', 'test'); 175 | expect(response.statusCode).toBe(200); 176 | }); 177 | 178 | test('should be 200 if value and properties are null', async () => { 179 | const response = await request(app) 180 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic') 181 | .set('Authorization', 'test'); 182 | expect(response.statusCode).toBe(200); 183 | }); 184 | 185 | test('should be 200 if value is null and properties is valid', async () => { 186 | const response = await request(app) 187 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&properties={"prop1":3}') 188 | .set('Authorization', 'test'); 189 | expect(response.statusCode).toBe(200); 190 | }); 191 | 192 | test('should be 200 if value and and properties are valid', async () => { 193 | const response = await request(app) 194 | .get('/client/track?key=my-key&event-type=my-event&traffic-type=my-traffic&properties={"prop1":3}&value=3.0') 195 | .set('Authorization', 'test'); 196 | expect(response.statusCode).toBe(200); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /client/client.controller.js: -------------------------------------------------------------------------------- 1 | // Own modules 2 | const { parseKey, filterFeatureFlagsByTT } = require('./common'); 3 | const environmentManager = require('../environmentManager').getInstance(); 4 | 5 | /** 6 | * getTreatment evaluates a given split-name 7 | * @param {*} req 8 | * @param {*} res 9 | */ 10 | const getTreatment = async (req, res) => { 11 | const client = environmentManager.getClient(req.headers.authorization); 12 | const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); 13 | const featureFlag = req.splitio.featureFlagName; 14 | const attributes = req.splitio.attributes; 15 | 16 | try { 17 | const evaluationResult = await client.getTreatment(key, featureFlag, attributes); 18 | environmentManager.updateLastEvaluation(req.headers.authorization); 19 | 20 | res.send({ 21 | splitName: featureFlag, 22 | treatment: evaluationResult, 23 | }); 24 | } catch (error) { 25 | res.status(500).send({error}); 26 | } 27 | }; 28 | 29 | /** 30 | * getTreatmentWithConfig evaluates a given split-name and returns config also 31 | * @param {*} req 32 | * @param {*} res 33 | */ 34 | const getTreatmentWithConfig = async (req, res) => { 35 | const client = environmentManager.getClient(req.headers.authorization); 36 | const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); 37 | const featureFlag = req.splitio.featureFlagName; 38 | const attributes = req.splitio.attributes; 39 | 40 | try { 41 | const evaluationResult = await client.getTreatmentWithConfig(key, featureFlag, attributes); 42 | environmentManager.updateLastEvaluation(req.headers.authorization); 43 | 44 | res.send({ 45 | splitName: featureFlag, 46 | treatment: evaluationResult.treatment, 47 | config: evaluationResult.config, 48 | }); 49 | } catch (error) { 50 | res.status(500).send({error}); 51 | } 52 | }; 53 | 54 | /** 55 | * getTreatments evaluates an array of split-names 56 | * @param {*} req 57 | * @param {*} res 58 | */ 59 | const getTreatments = async (req, res) => { 60 | const client = environmentManager.getClient(req.headers.authorization); 61 | const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); 62 | const featureFlags = req.splitio.featureFlagNames; 63 | const attributes = req.splitio.attributes; 64 | 65 | try { 66 | const evaluationResults = await client.getTreatments(key, featureFlags, attributes); 67 | environmentManager.updateLastEvaluation(req.headers.authorization); 68 | 69 | const result = {}; 70 | Object.keys(evaluationResults).forEach(featureFlag => { 71 | result[featureFlag] = { 72 | treatment: evaluationResults[featureFlag], 73 | }; 74 | }); 75 | 76 | res.send(result); 77 | } catch (error) { 78 | res.status(500).send({error}); 79 | } 80 | }; 81 | 82 | /** 83 | * getTreatmentsWithConfig evaluates an array of split-names and returns configs also 84 | * @param {*} req 85 | * @param {*} res 86 | */ 87 | const getTreatmentsWithConfig = async (req, res) => { 88 | const client = environmentManager.getClient(req.headers.authorization); 89 | const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); 90 | const featureFlags = req.splitio.featureFlagNames; 91 | const attributes = req.splitio.attributes; 92 | 93 | try { 94 | const evaluationResults = await client.getTreatmentsWithConfig(key, featureFlags, attributes); 95 | environmentManager.updateLastEvaluation(req.headers.authorization); 96 | 97 | res.send(evaluationResults); 98 | } catch (error) { 99 | res.status(500).send({error}); 100 | } 101 | }; 102 | 103 | /** 104 | * getTreatmentsByFlagSets evaluates an array of flag sets and returns configs also 105 | * @param {*} req 106 | * @param {*} res 107 | */ 108 | const getTreatmentsByFlagSets = async (req, res) => { 109 | const client = environmentManager.getClient(req.headers.authorization); 110 | const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); 111 | const flagSets = req.splitio.flagSetNames; 112 | const attributes = req.splitio.attributes; 113 | 114 | try { 115 | const evaluationResults = await client.getTreatmentsByFlagSets(key, flagSets, attributes); 116 | environmentManager.updateLastEvaluation(req.headers.authorization); 117 | 118 | const result = {}; 119 | Object.keys(evaluationResults).forEach(featureFlag => { 120 | result[featureFlag] = { 121 | treatment: evaluationResults[featureFlag], 122 | }; 123 | }); 124 | 125 | res.send(result); 126 | } catch (error) { 127 | res.status(500).send({error}); 128 | } 129 | }; 130 | 131 | /** 132 | * getTreatmentsWithConfigByFlagSets evaluates an array of flag sets 133 | * @param {*} req 134 | * @param {*} res 135 | */ 136 | const getTreatmentsWithConfigByFlagSets = async (req, res) => { 137 | const client = environmentManager.getClient(req.headers.authorization); 138 | const key = parseKey(req.splitio.matchingKey, req.splitio.bucketingKey); 139 | const flagSets = req.splitio.flagSetNames; 140 | const attributes = req.splitio.attributes; 141 | 142 | try { 143 | const evaluationResults = await client.getTreatmentsWithConfigByFlagSets(key, flagSets, attributes); 144 | environmentManager.updateLastEvaluation(req.headers.authorization); 145 | 146 | const result = evaluationResults; 147 | 148 | res.send(result); 149 | } catch (error) { 150 | res.status(500).send({error}); 151 | } 152 | }; 153 | 154 | /** 155 | * track events tracking 156 | * @param {*} req 157 | * @param {*} res 158 | */ 159 | const track = async (req, res) => { 160 | const client = environmentManager.getClient(req.headers.authorization); 161 | const key = req.splitio.key; 162 | const trafficType = req.splitio.trafficType; 163 | const eventType = req.splitio.eventType; 164 | const value = req.splitio.value; 165 | const properties = req.splitio.properties; 166 | 167 | try { 168 | const tracked = await client.track(key, trafficType, eventType, value, properties); 169 | return tracked ? res.status(200).send('Successfully queued event') : res.status(400); 170 | } catch (error) { 171 | res.status(500).send({error}); 172 | } 173 | }; 174 | 175 | /** 176 | * allTreatments matches featureFlags for passed trafficType and evaluates with passed key 177 | * @param {Object} keys 178 | * @param {Object} attributes 179 | */ 180 | const allTreatments = async (authorization, keys, attributes) => { 181 | const manager = environmentManager.getManager(authorization); 182 | const client = environmentManager.getClient(authorization); 183 | try { 184 | // Grabs featureFlags from Manager 185 | const featureFlagViews = await manager.splits(); 186 | 187 | // Makes multiple evaluations for each (trafficType, key) 188 | const evaluations = {}; 189 | for (let i=0; i< keys.length; i++) { 190 | const key = keys[i]; 191 | const featureFlagNames = filterFeatureFlagsByTT(featureFlagViews, key.trafficType); 192 | const evaluation = await client.getTreatmentsWithConfig( 193 | parseKey(key.matchingKey, key.bucketingKey), 194 | featureFlagNames, 195 | attributes); 196 | // Saves result for each trafficType 197 | evaluations[key.trafficType] = evaluation; 198 | } 199 | environmentManager.updateLastEvaluation(authorization); 200 | 201 | return evaluations; 202 | } catch (error) { 203 | throw Error('Error getting treatments'); 204 | } 205 | }; 206 | 207 | /** 208 | * getAllTreatmentsWithConfig returns the allTreatments evaluations with configs 209 | * @param {*} req 210 | * @param {*} res 211 | */ 212 | const getAllTreatmentsWithConfig = async (req, res) => { 213 | const keys = req.splitio.keys; 214 | const attributes = req.splitio.attributes; 215 | 216 | try { 217 | const treatments = await allTreatments(req.headers.authorization, keys, attributes); 218 | environmentManager.updateLastEvaluation(req.headers.authorization); 219 | res.send(treatments); 220 | } catch (error) { 221 | res.status(500).send({error}); 222 | } 223 | }; 224 | 225 | /** 226 | * getAllTreatments returns the allTreatments evaluations 227 | * @param {*} req 228 | * @param {*} res 229 | */ 230 | const getAllTreatments = async (req, res) => { 231 | const keys = req.splitio.keys; 232 | const attributes = req.splitio.attributes; 233 | 234 | try { 235 | const treatments = await allTreatments(req.headers.authorization, keys, attributes); 236 | // Erases the config property for treatments 237 | const trafficTypes = Object.keys(treatments); 238 | trafficTypes.forEach(trafficType => { 239 | const featureFlagNames = Object.keys(treatments[trafficType]); 240 | if (featureFlagNames.length > 0) { 241 | Object.keys(treatments[trafficType]).forEach(featureFlag => { 242 | delete treatments[trafficType][featureFlag].config; 243 | }); 244 | } 245 | }); 246 | environmentManager.updateLastEvaluation(req.headers.authorization); 247 | 248 | res.send(treatments); 249 | } catch (error) { 250 | res.status(500).send({error}); 251 | } 252 | }; 253 | 254 | module.exports = { 255 | getTreatment, 256 | getTreatments, 257 | getTreatmentWithConfig, 258 | getTreatmentsWithConfig, 259 | getTreatmentsByFlagSets, 260 | getTreatmentsWithConfigByFlagSets, 261 | getAllTreatments, 262 | getAllTreatmentsWithConfig, 263 | track, 264 | }; 265 | -------------------------------------------------------------------------------- /client/client.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const keyValidator = require('../utils/inputValidation/key'); 4 | const splitValidator = require('../utils/inputValidation/split'); 5 | const flagSetsValidator = require('../utils/inputValidation/flagSets'); 6 | const splitsValidator = require('../utils/inputValidation/splits'); 7 | const attributesValidator = require('../utils/inputValidation/attributes'); 8 | const trafficTypeValidator = require('../utils/inputValidation/trafficType'); 9 | const eventTypeValidator = require('../utils/inputValidation/eventType'); 10 | const valueValidator = require('../utils/inputValidation/value'); 11 | const propertiesValidator = require('../utils/inputValidation/properties'); 12 | const keysValidator = require('../utils/inputValidation/keys'); 13 | const clientController = require('./client.controller'); 14 | const { parseValidators } = require('../utils/utils'); 15 | 16 | /** 17 | * treatmentValidation performs input validation for treatment call. 18 | * @param {object} req 19 | * @param {object} res 20 | * @param {function} next 21 | */ 22 | const treatmentValidation = (req, res, next) => { 23 | const matchingKeyValidation = keyValidator(req.query.key, 'key'); 24 | const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; 25 | const featureFlagNameValidation = splitValidator(req.query['split-name']); 26 | const attributesValidation = attributesValidator(req.query.attributes); 27 | 28 | const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, featureFlagNameValidation, attributesValidation]); 29 | if (error.length) { 30 | return res 31 | .status(400) 32 | .send({ 33 | error, 34 | }); 35 | } else { 36 | req.splitio = { 37 | matchingKey: matchingKeyValidation.value, 38 | featureFlagName: featureFlagNameValidation.value, 39 | attributes: attributesValidation.value, 40 | }; 41 | 42 | if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; 43 | } 44 | 45 | next(); 46 | }; 47 | 48 | /** 49 | * treatmentsValidation performs input validation for treatments call. 50 | * @param {object} req 51 | * @param {object} res 52 | * @param {function} next 53 | */ 54 | const treatmentsValidation = (req, res, next) => { 55 | const matchingKeyValidation = keyValidator(req.query.key, 'key'); 56 | const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; 57 | const featureFlagsNameValidation = splitsValidator(req.query['split-names']); 58 | const attributesValidation = attributesValidator(req.query.attributes); 59 | 60 | const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, featureFlagsNameValidation, attributesValidation]); 61 | if (error.length) { 62 | return res 63 | .status(400) 64 | .send({ 65 | error, 66 | }); 67 | } else { 68 | req.splitio = { 69 | matchingKey: matchingKeyValidation.value, 70 | featureFlagNames: featureFlagsNameValidation.value, 71 | attributes: attributesValidation.value, 72 | }; 73 | 74 | if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; 75 | } 76 | 77 | next(); 78 | }; 79 | 80 | /** 81 | * flagSetsValidation performs input validation for flag sets call. 82 | * @param {object} req 83 | * @param {object} res 84 | * @param {function} next 85 | */ 86 | const flagSetsValidation = (req, res, next) => { 87 | const matchingKeyValidation = keyValidator(req.query.key, 'key'); 88 | const bucketingKeyValidation = req.query['bucketing-key'] !== undefined ? keyValidator(req.query['bucketing-key'], 'bucketing-key') : null; 89 | const flagSetNameValidation = flagSetsValidator(req.query['flag-sets']); 90 | const attributesValidation = attributesValidator(req.query.attributes); 91 | 92 | const error = parseValidators([matchingKeyValidation, bucketingKeyValidation, flagSetNameValidation, attributesValidation]); 93 | if (error.length) { 94 | return res 95 | .status(400) 96 | .send({ 97 | error, 98 | }); 99 | } else { 100 | req.splitio = { 101 | matchingKey: matchingKeyValidation.value, 102 | flagSetNames: flagSetNameValidation.value, 103 | attributes: attributesValidation.value, 104 | }; 105 | 106 | if (bucketingKeyValidation && bucketingKeyValidation.valid) req.splitio.bucketingKey = bucketingKeyValidation.value; 107 | } 108 | 109 | next(); 110 | }; 111 | 112 | /** 113 | * trackValidation performs input validation for event tracking calls. 114 | * @param {object} req 115 | * @param {object} res 116 | * @param {function} next 117 | */ 118 | const trackValidation = (req, res, next) => { 119 | const keyValidation = keyValidator(req.query.key, 'key'); 120 | const trafficTypeValidation = trafficTypeValidator(req.query['traffic-type']); 121 | const eventTypeValidation = eventTypeValidator(req.query['event-type']); 122 | const valueValidation = valueValidator(req.query.value); 123 | const propertiesValidation = propertiesValidator(req.query.properties); 124 | 125 | const error = parseValidators([keyValidation, trafficTypeValidation, eventTypeValidation, valueValidation, propertiesValidation]); 126 | if (error.length) { 127 | return res 128 | .status(400) 129 | .send({ 130 | error, 131 | }); 132 | } else { 133 | req.splitio = { 134 | key: keyValidation.value, 135 | trafficType: trafficTypeValidation.value, 136 | eventType: eventTypeValidation.value, 137 | value: valueValidation.value, 138 | properties: propertiesValidation.value, 139 | }; 140 | } 141 | 142 | next(); 143 | }; 144 | 145 | /** 146 | * allTreatmentValidation performs input validation for all treatments call. 147 | * @param {object} req 148 | * @param {object} res 149 | * @param {function} next 150 | */ 151 | const allTreatmentValidation = (req, res, next) => { 152 | const keysValidation = keysValidator(req.query.keys); 153 | const attributesValidation = attributesValidator(req.query.attributes); 154 | 155 | const error = parseValidators([keysValidation, attributesValidation]); 156 | if (error.length) { 157 | return res 158 | .status(400) 159 | .send({ 160 | error, 161 | }); 162 | } else { 163 | req.splitio = { 164 | keys: keysValidation.value, 165 | attributes: attributesValidation.value, 166 | }; 167 | } 168 | 169 | next(); 170 | }; 171 | 172 | // Simple method to reuse the full logic of the GET version of get treatment operations, 173 | // by just connecting the json payload parsed on the right spot. 174 | const fwdAttributesFromPost = function parseAttributesMiddleware(req, res, next) { 175 | req.query.attributes = req.body.attributes; 176 | 177 | next(); 178 | }; 179 | 180 | const handleBodyParserErr = function handleBodyParserErr(error, req, res, next) { 181 | if (error) { 182 | return res 183 | .status(400) 184 | .send({ 185 | error, 186 | }); 187 | } 188 | 189 | next(); 190 | }; 191 | 192 | // Getting treatments regularly 193 | router.get('/get-treatment', treatmentValidation, clientController.getTreatment); 194 | router.get('/get-treatment-with-config', treatmentValidation, clientController.getTreatmentWithConfig); 195 | router.get('/get-treatments', treatmentsValidation, clientController.getTreatments); 196 | router.get('/get-treatments-with-config', treatmentsValidation, clientController.getTreatmentsWithConfig); 197 | router.get('/get-treatments-by-sets', flagSetsValidation, clientController.getTreatmentsByFlagSets); 198 | router.get('/get-treatments-with-config-by-sets', flagSetsValidation, clientController.getTreatmentsWithConfigByFlagSets); 199 | router.get('/get-all-treatments', allTreatmentValidation, clientController.getAllTreatments); 200 | router.get('/get-all-treatments-with-config', allTreatmentValidation, clientController.getAllTreatmentsWithConfig); 201 | 202 | // Getting treatments as POST's for big attribute sets 203 | const JSON_PARSE_OPTS = { limit: '300kb' }; 204 | router.post('/get-treatment',express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatment); 205 | router.post('/get-treatment-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentValidation, clientController.getTreatmentWithConfig); 206 | router.post('/get-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatments); 207 | router.post('/get-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, treatmentsValidation, clientController.getTreatmentsWithConfig); 208 | router.post('/get-treatments-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsByFlagSets); 209 | router.post('/get-treatments-with-config-by-sets', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, flagSetsValidation, clientController.getTreatmentsWithConfigByFlagSets); 210 | router.post('/get-all-treatments', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatments); 211 | router.post('/get-all-treatments-with-config', express.json(JSON_PARSE_OPTS), fwdAttributesFromPost, handleBodyParserErr, allTreatmentValidation, clientController.getAllTreatmentsWithConfig); 212 | 213 | // Other methods 214 | router.get('/track', trackValidation, clientController.track); 215 | 216 | module.exports = router; 217 | -------------------------------------------------------------------------------- /client/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * filterFeatureFlagsByTT Reduces a collection of featureFlag views to a list of names of the featureFlags 3 | * corresponding to the given traffic type. 4 | * @param object featureFlagViews 5 | * @param string trafficType 6 | */ 7 | const filterFeatureFlagsByTT = (featureFlagViews, trafficType) => featureFlagViews.reduce((acc, view) => { 8 | if (view.trafficType === trafficType) { 9 | acc.push(view.name); 10 | } 11 | return acc; 12 | }, []); 13 | 14 | /** 15 | * parseKey Given a pair of values, make the processing to create the SplitKey value. 16 | * @param string matchingKey 17 | * @param string bucketingKey 18 | */ 19 | const parseKey = (matchingKey, bucketingKey) => !bucketingKey ? matchingKey : { 20 | matchingKey, 21 | bucketingKey, 22 | }; 23 | 24 | module.exports = { 25 | filterFeatureFlagsByTT, 26 | parseKey, 27 | }; 28 | -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | // 2 | // NODEJS SDK - IN MEMORY STORAGE - STANDALONE MODE 3 | // 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | sdk: { 8 | // In case someone uses localhost with the same file 9 | features: path.join(__dirname, 'split.yml'), 10 | }, 11 | // Block the ExpressJS server till the SDK is ready. 12 | blockUntilReady: true, 13 | // Impression Listener Webhook 14 | impressionsPerPost: 500, 15 | impressionsSendRate: 15000, 16 | }; 17 | -------------------------------------------------------------------------------- /config/test.js: -------------------------------------------------------------------------------- 1 | // 2 | // NODEJS SDK - IN MEMORY STORAGE - STANDALONE MODE 3 | // 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | sdk: { 8 | // In case someone uses localhost with the same file 9 | features: path.join(__dirname, 'split.yml'), 10 | }, 11 | // Block the ExpressJS server till the SDK is ready. 12 | blockUntilReady: true, 13 | // Impression Listener Webhook 14 | impressionsPerPost: 5, 15 | impressionsSendRate: 600, 16 | }; 17 | -------------------------------------------------------------------------------- /environmentManager/__tests__/clientReadines.test.js: -------------------------------------------------------------------------------- 1 | // Environment configurations 2 | const threeReady = JSON.stringify([ 3 | { API_KEY: 'localhost', AUTH_TOKEN: 'ready1' }, 4 | { API_KEY: 'apikey1', AUTH_TOKEN: 'ready2' }, 5 | { API_KEY: 'apikey2', AUTH_TOKEN: 'ready3' } 6 | ]); 7 | 8 | const twoReadyOneTimedOut = JSON.stringify([ 9 | { API_KEY: 'localhost', AUTH_TOKEN: 'ready1' }, 10 | { API_KEY: 'apikey1', AUTH_TOKEN: 'ready2' }, 11 | { API_KEY: 'timeout', AUTH_TOKEN: 'timedOut' } 12 | ]); 13 | 14 | const oneReadyTwoTimedOut = JSON.stringify([ 15 | { API_KEY: 'localhost', AUTH_TOKEN: 'ready1' }, 16 | { API_KEY: 'timeout', AUTH_TOKEN: 'timedOut1' }, 17 | { API_KEY: 'timeout', AUTH_TOKEN: 'timedOut2' } 18 | ]); 19 | 20 | const threeTimedOut = JSON.stringify([ 21 | { API_KEY: 'timeout', AUTH_TOKEN: 'timedOut1' }, 22 | { API_KEY: 'timeout', AUTH_TOKEN: 'timedOut2' }, 23 | { API_KEY: 'timeout', AUTH_TOKEN: 'timedOut3' } 24 | ]); 25 | 26 | // Mock fetch to response 404 to any http request 27 | jest.mock('node-fetch', () => { 28 | return jest.fn().mockImplementation(() => { 29 | return Promise.reject({ response: { status: 404, response: 'response'}}); 30 | }); 31 | }); 32 | 33 | // Test environmentManager client readiness 34 | describe('environmentManager', () => { 35 | 36 | beforeEach(() => { 37 | jest.resetModules(); 38 | jest.clearAllMocks(); 39 | }); 40 | 41 | afterAll(() => { 42 | // Unmock fetch 43 | jest.unmock('node-fetch'); 44 | }); 45 | 46 | describe('clientReadiness', () => { 47 | 48 | // If at least one client is ready, environmentManager.ready() should return true to initialize evaluator 49 | test('Three clients ready', (done) => { 50 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 51 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=threeReady; 52 | const environmentManagerFactory = require('../'); 53 | const environmentManager = environmentManagerFactory.getInstance(); 54 | environmentManager.ready().then(ready => { 55 | expect(ready).toBe(true); 56 | expect(environmentManager.getClient('ready1').isClientReady).toBe(true); 57 | expect(environmentManager.getClient('ready2').isClientReady).toBe(true); 58 | expect(environmentManager.getClient('ready3').isClientReady).toBe(true); 59 | done(); 60 | }); 61 | }); 62 | 63 | // If at least one client is ready, environmentManager.ready() should return true to initialize evaluator 64 | test('Two clients ready, one timed out', (done) => { 65 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 66 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=twoReadyOneTimedOut; 67 | const environmentManagerFactory = require('../'); 68 | const environmentManager = environmentManagerFactory.getInstance(); 69 | environmentManager.ready().then(ready => { 70 | expect(ready).toBe(true); 71 | expect(environmentManager.getClient('ready1').isClientReady).toBe(true); 72 | expect(environmentManager.getClient('ready2').isClientReady).toBe(true); 73 | expect(environmentManager.getClient('timedOut').isClientReady).toBe(false); 74 | done(); 75 | }); 76 | }); 77 | 78 | // If at least one client is ready, environmentManager.ready() should return true to initialize evaluator 79 | test('One client ready, two timed out', (done) => { 80 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 81 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=oneReadyTwoTimedOut; 82 | const environmentManagerFactory = require('../'); 83 | const environmentManager = environmentManagerFactory.getInstance(); 84 | environmentManager.ready().then(ready => { 85 | expect(ready).toBe(true); 86 | expect(environmentManager.getClient('ready1').isClientReady).toBe(true); 87 | expect(environmentManager.getClient('timedOut1').isClientReady).toBe(false); 88 | expect(environmentManager.getClient('timedOut2').isClientReady).toBe(false); 89 | done(); 90 | }); 91 | }); 92 | 93 | // If every client timed out, environmentManager.ready() should return false to avoid evaluator initialization 94 | test('Three clients timed out', (done) => { 95 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 96 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=threeTimedOut; 97 | const environmentManagerFactory = require('../'); 98 | const environmentManager = environmentManagerFactory.getInstance(); 99 | environmentManager.ready().then(ready => { 100 | expect(ready).toBe(false); 101 | expect(environmentManager.getClient('timedOut1').isClientReady).toBe(false); 102 | expect(environmentManager.getClient('timedOut2').isClientReady).toBe(false); 103 | expect(environmentManager.getClient('timedOut3').isClientReady).toBe(false); 104 | done(); 105 | }); 106 | }); 107 | }); 108 | }); -------------------------------------------------------------------------------- /environmentManager/__tests__/globalConfig.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { core, scheduler, storage, urls, startup, sync, integrations } = require('../../utils/mocks'); 3 | 4 | // Environment configurations 5 | const environmentsConfig = [ 6 | { API_KEY: 'test1', AUTH_TOKEN: 'ready2' }, 7 | { API_KEY: 'test2', AUTH_TOKEN: 'ready3' } 8 | ]; 9 | 10 | // Mock fetch to response 404 to any http request 11 | jest.mock('node-fetch', () => { 12 | return jest.fn().mockImplementation(() => { 13 | return Promise.reject({ response: { status: 404, response: 'response'}}); 14 | }); 15 | }); 16 | 17 | // Test environmentManager global config 18 | describe('environmentManager', () => { 19 | 20 | beforeEach(() => { 21 | jest.resetModules(); 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | afterAll(() => { 26 | // Unmock fetch 27 | jest.unmock('node-fetch'); 28 | }); 29 | 30 | describe('global config', () => { 31 | 32 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ 33 | core: core, 34 | scheduler: scheduler, 35 | storage: storage, 36 | urls: urls, 37 | startup: startup, 38 | sync: sync, 39 | mode: 'consumer', 40 | debug: false, 41 | streamingEnabled: false, 42 | integrations: integrations, 43 | }); 44 | 45 | test('', async () => { 46 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 47 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=JSON.stringify(environmentsConfig); 48 | const environmentManagerFactory = require('../'); 49 | const environmentManager = environmentManagerFactory.getInstance(); 50 | environmentsConfig.forEach(environment => { 51 | const factorySettings = environmentManager.getFactory(environment.AUTH_TOKEN).settings; 52 | // Authorization key should be the one on environments config 53 | expect(factorySettings.core.authorizationKey).toBe(environment.API_KEY); 54 | // IPAddressesEnabled can be configured only with SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED 55 | expect(factorySettings.core.IPAddressesEnabled).toBe(true); 56 | // Mode should be always standalone 57 | expect(factorySettings.mode).toBe('standalone'); 58 | // Sync should be always enabled 59 | expect(factorySettings.sync.enabled).toBe(true); 60 | // Integrations should be always removed 61 | expect(factorySettings.integrations).toBe(undefined); 62 | // Storage should be always MEMORY 63 | expect(factorySettings.storage.type).toBe('MEMORY'); 64 | // impressionsMode should be NONE as configured in global config 65 | expect(factorySettings.sync.impressionsMode).toBe('NONE'); 66 | }); 67 | await environmentManagerFactory.destroy(); 68 | }); 69 | }); 70 | 71 | describe('flag sets', () => { 72 | test('Environment manager should initialize for legacy configuration without filters', async () => { 73 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 74 | process.env.SPLIT_EVALUATOR_AUTH_TOKEN = 'test'; 75 | process.env.SPLIT_EVALUATOR_API_KEY = 'test'; 76 | const environmentManagerFactory = require('../'); 77 | expect(() => environmentManagerFactory.getInstance()).not.toThrow(); 78 | expect(environmentManagerFactory.hasInstance()).toBe(true); 79 | await environmentManagerFactory.destroy(); 80 | }); 81 | 82 | test('Environment manager should throw an error if is initialized with environments and filters on global config', async () => { 83 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ 84 | urls: urls, 85 | sync: { 86 | splitFilters: [{type: 'bySet', values: ['set_a', 'set_b']}], 87 | }, 88 | }); 89 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = JSON.stringify(environmentsConfig); 90 | expect(() => require('../')).toThrow(); 91 | }); 92 | 93 | test('Environment manager should initialize for legacy configuration with filters', async () => { 94 | delete process.env.SPLIT_EVALUATOR_ENVIRONMENTS; 95 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ 96 | urls: urls, 97 | sync: { 98 | splitFilters: [{type: 'bySet', values: ['set_a', 'set_b']}], 99 | }, 100 | }); 101 | 102 | const environmentManagerFactory = require('../'); 103 | const environmentManager = environmentManagerFactory.getInstance(); 104 | const factorySettings = environmentManager.getFactory(process.env.SPLIT_EVALUATOR_AUTH_TOKEN).settings; 105 | expect(factorySettings.sync.splitFilters).toEqual([{type: 'bySet', values: ['set_a', 'set_b']}]); 106 | await environmentManagerFactory.destroy(); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /environmentManager/__tests__/manager.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | 4 | jest.mock('node-fetch', () => { 5 | return jest.fn().mockImplementation((url) => { 6 | 7 | const sdkUrl = 'https://sdk.test.io/api/splitChanges?s=1.1&since=-1'; 8 | const splitChange2 = require('../../utils/mocks/splitchanges.since.-1.till.1602796638344.json'); 9 | if (url.startsWith(sdkUrl)) return Promise.resolve({ status: 200, json: () => (splitChange2), ok: true }); 10 | 11 | return Promise.resolve({ status: 200, json: () => ({}), ok: true }); 12 | }); 13 | }); 14 | 15 | // Multiple environment - manager endpoints 16 | describe('environmentManager - manager endpoints', () => { 17 | 18 | beforeEach(() => { 19 | jest.resetModules(); 20 | jest.clearAllMocks(); 21 | }); 22 | 23 | afterAll(() => { 24 | // Unmock fetch 25 | jest.unmock('node-fetch'); 26 | }); 27 | 28 | // splits 29 | test('[/splits] should be 200 if is valid authToken and return feature flags on split2 yaml file for key_red', async () => { 30 | const response = await request(app) 31 | .get('/manager/splits') 32 | .set('Authorization', 'key_red'); 33 | expect(response.statusCode).toBe(200); 34 | expect(response.body.splits.map(split => {return split.name;})) 35 | .toEqual( 36 | ['testing_split_red', 'testing_split_color', 'testing_split_only_wl', 'testing_split_with_wl', 'testing_split_with_config'] 37 | ); 38 | }); 39 | 40 | test('[/splits] should be 200 if is valid authToken and return feature flags on split1 yaml file for key_blue', async () => { 41 | const response = await request(app) 42 | .get('/manager/splits') 43 | .set('Authorization', 'key_blue'); 44 | expect(response.statusCode).toBe(200); 45 | expect(response.body.splits.map(split => {return split.name;})) 46 | .toEqual( 47 | ['testing_split_blue', 'testing_split_color', 'testing_split_only_wl', 'testing_split_with_wl', 'testing_split_with_config'] 48 | ); 49 | }); 50 | 51 | test('[/splits] should be 401 if is non existent authToken and return unauthorized error', async () => { 52 | const response = await request(app) 53 | .get('/manager/splits') 54 | .set('Authorization', 'non-existent'); 55 | expect(response.statusCode).toBe(401); 56 | expect(response.body).toEqual({'error':'Unauthorized'}); 57 | }); 58 | 59 | test('[/splits] should be 200 if is valid authToken and return feature flags on set set_green for key_green', async () => { 60 | const response = await request(app) 61 | .get('/manager/splits') 62 | .set('Authorization', 'key_green'); 63 | expect(response.statusCode).toBe(200); 64 | expect(response.body.splits.map(flag => {return flag.name;})) 65 | .toEqual( 66 | ['test_green', 'test_color', 'test_green_config'] 67 | ); 68 | }); 69 | 70 | test('[/splits] should be 200 if is valid authToken and return feature flags on set set_purple for key_purple', async () => { 71 | const response = await request(app) 72 | .get('/manager/splits') 73 | .set('Authorization', 'key_purple'); 74 | expect(response.statusCode).toBe(200); 75 | expect(response.body.splits.map(flag => {return flag.name;})) 76 | .toEqual( 77 | ['test_color', 'test_purple', 'test_purple_config'] 78 | ); 79 | }); 80 | 81 | test('[/splits] should be 200 if is valid authToken and return feature flags on sets set_green & set_purple file for key_pink', async () => { 82 | const response = await request(app) 83 | .get('/manager/splits') 84 | .set('Authorization', 'key_pink'); 85 | expect(response.statusCode).toBe(200); 86 | expect(response.body.splits.map(flag => {return flag.name;})) 87 | .toEqual( 88 | ['test_green', 'test_color', 'test_green_config', 'test_purple', 'test_purple_config'] 89 | ); 90 | }); 91 | 92 | // split 93 | test('[/split] should be 200 if is valid authToken and return feature flag testing_split_red for key_red', async () => { 94 | const response = await request(app) 95 | .get('/manager/split?split-name=testing_split_red') 96 | .set('Authorization', 'key_red'); 97 | expect(response.statusCode).toBe(200); 98 | expect(response.body.name).toEqual('testing_split_red'); 99 | }); 100 | 101 | test('[/split] should be 200 if is valid authToken and return feature flag testing_split_blue for key_blue', async () => { 102 | const response = await request(app) 103 | .get('/manager/split?split-name=testing_split_blue') 104 | .set('Authorization', 'key_blue'); 105 | expect(response.statusCode).toBe(200); 106 | expect(response.body.name).toEqual('testing_split_blue'); 107 | }); 108 | 109 | test('[/split] should be 401 if is non existent authToken and return unauthorized error', async () => { 110 | const response = await request(app) 111 | .get('/manager/split?split-name=testing_split_blue') 112 | .set('Authorization', 'non-existent'); 113 | expect(response.statusCode).toBe(401); 114 | expect(response.body).toEqual({'error':'Unauthorized'}); 115 | }); 116 | 117 | test('[/split] should be 200 if is valid authToken and return feature flag test_green for key_green', async () => { 118 | const response = await request(app) 119 | .get('/manager/split?split-name=test_green') 120 | .set('Authorization', 'key_green'); 121 | expect(response.statusCode).toBe(200); 122 | expect(response.body.name).toEqual('test_green'); 123 | expect(response.body.sets).toEqual(['set_green']); 124 | }); 125 | 126 | test('[/split] should be 200 if is valid authToken and return feature flag test_purple for key_purple', async () => { 127 | const response = await request(app) 128 | .get('/manager/split?split-name=test_purple') 129 | .set('Authorization', 'key_purple'); 130 | expect(response.statusCode).toBe(200); 131 | expect(response.body.name).toEqual('test_purple'); 132 | expect(response.body.sets).toEqual(['set_purple']); 133 | }); 134 | 135 | test('[/split] should be 404 if is valid authToken and return 404 for test_green using key_purple', async () => { 136 | const response = await request(app) 137 | .get('/manager/split?split-name=test_green') 138 | .set('Authorization', 'key_purple'); 139 | expect(response.statusCode).toBe(404); 140 | }); 141 | 142 | test('[/split] should be 200 if is valid authToken and return feature flag test_green for key_pink', async () => { 143 | const response = await request(app) 144 | .get('/manager/split?split-name=test_green') 145 | .set('Authorization', 'key_pink'); 146 | expect(response.statusCode).toBe(200); 147 | expect(response.body.name).toEqual('test_green'); 148 | expect(response.body.sets).toEqual(['set_green']); 149 | }); 150 | 151 | test('[/split] should be 200 if is valid authToken and return feature flag test_purple for key_pink', async () => { 152 | const response = await request(app) 153 | .get('/manager/split?split-name=test_purple') 154 | .set('Authorization', 'key_pink'); 155 | expect(response.statusCode).toBe(200); 156 | expect(response.body.name).toEqual('test_purple'); 157 | expect(response.body.sets).toEqual(['set_purple']); 158 | }); 159 | 160 | 161 | 162 | // names 163 | test('[/names] should be 200 if is valid authToken and return feature flags in split2.yml', async () => { 164 | const response = await request(app) 165 | .get('/manager/names') 166 | .set('Authorization', 'key_red'); 167 | expect(response.statusCode).toBe(200); 168 | expect(response.body.splits) 169 | .toEqual( 170 | ['testing_split_red', 'testing_split_color', 'testing_split_only_wl', 'testing_split_with_wl', 'testing_split_with_config'] 171 | ); 172 | }); 173 | 174 | test('[/names] should be 200 if is valid authToken and return feature flags in split1.yml', async () => { 175 | const response = await request(app) 176 | .get('/manager/names') 177 | .set('Authorization', 'key_blue'); 178 | expect(response.statusCode).toBe(200); 179 | expect(response.body.splits) 180 | .toEqual( 181 | ['testing_split_blue', 'testing_split_color', 'testing_split_only_wl', 'testing_split_with_wl', 'testing_split_with_config'] 182 | ); 183 | }); 184 | 185 | test('[/names] should be 401 if is non existent authToken and return unauthorized error', async () => { 186 | const response = await request(app) 187 | .get('/manager/names') 188 | .set('Authorization', 'non-existent'); 189 | expect(response.statusCode).toBe(401); 190 | expect(response.body).toEqual({'error':'Unauthorized'}); 191 | }); 192 | 193 | test('[/names] should be 200 if is valid authToken and return feature flags on set set_green for key_green', async () => { 194 | const response = await request(app) 195 | .get('/manager/names') 196 | .set('Authorization', 'key_green'); 197 | expect(response.statusCode).toBe(200); 198 | expect(response.body.splits) 199 | .toEqual( 200 | ['test_green', 'test_color', 'test_green_config'] 201 | ); 202 | }); 203 | 204 | test('[/names] should be 200 if is valid authToken and return feature flags on set set_purple for key_purple', async () => { 205 | const response = await request(app) 206 | .get('/manager/names') 207 | .set('Authorization', 'key_purple'); 208 | expect(response.statusCode).toBe(200); 209 | expect(response.body.splits) 210 | .toEqual( 211 | ['test_color', 'test_purple', 'test_purple_config'] 212 | ); 213 | }); 214 | 215 | test('[/names] should be 200 if is valid authToken and return feature flags on sets set_green & set_purple file for key_pink', async () => { 216 | const response = await request(app) 217 | .get('/manager/names') 218 | .set('Authorization', 'key_pink'); 219 | expect(response.statusCode).toBe(200); 220 | expect(response.body.splits) 221 | .toEqual( 222 | ['test_green', 'test_color', 'test_green_config', 'test_purple', 'test_purple_config'] 223 | ); 224 | }); 225 | 226 | }); 227 | -------------------------------------------------------------------------------- /environmentManager/__tests__/validation.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | 3 | const validEnvironment = '[{"API_KEY":"localhost","AUTH_TOKEN":"test"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"},{"API_KEY":"apikey2","AUTH_TOKEN":"key_red"}]'; 4 | const environmentNotString = [{'API_KEY':'localhost','AUTH_TOKEN':'test'},{'API_KEY':'apikey1','AUTH_TOKEN':'key_blue'},{'API_KEY':'apikey2','AUTH_TOKEN':'key_red'}]; 5 | const environmentNotList1 = '{"API_KEY":"localhost","AUTH_TOKEN":"test"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"}'; 6 | const environmentNotList2 = '{"API_KEY":"localhost","AUTH_TOKEN":"test"}'; 7 | const environmentWithoutAuthToken1 = '[{"API_KEY":"localhost"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"}]'; 8 | const environmentWithoutAuthToken2 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1"}]'; 9 | const environmentWithoutApiKey1 = '[{"AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_blue"}]'; 10 | const environmentWithoutApiKey2 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"AUTH_TOKEN":"key_blue"}]'; 11 | const environmentWithDuplicatedAuthToken = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":"key_red"}]'; 12 | const environmentWithAuthTokenNotString1 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":123}]'; 13 | const environmentWithAuthTokenNotString2 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":{"key":"value"}}]'; 14 | const environmentWithAuthTokenNotString3 = '[{"API_KEY":"localhost","AUTH_TOKEN":"key_red"},{"API_KEY":"apikey1","AUTH_TOKEN":true}]'; 15 | const environmentWithAuthTokenEmpty = '[{"API_KEY":"localhost","AUTH_TOKEN":""},{"API_KEY":"apikey1","AUTH_TOKEN":"key_red"}]'; 16 | const environmentWithValidFlagSets = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_a,set_b"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; 17 | const environmentsWithValidFlagSets1 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_a"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red","FLAG_SET_FILTER":"set_x"}]'; 18 | const environmentsWithValidFlagSets2 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"set_1"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red","FLAG_SET_FILTER":"set_c"}]'; 19 | const environmentWithInvalidFlagSets1 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":"Set_3,_set_4"},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; 20 | const environmentWithInvalidFlagSets2 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":["set_y","set_z"]},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; 21 | const environmentWithInvalidFlagSets3 = '[{"API_KEY":"key_green","AUTH_TOKEN":"key_green","FLAG_SET_FILTER":set_t},{"API_KEY":"key_red","AUTH_TOKEN":"key_red"}]'; 22 | 23 | // Multiple environment - client endpoints 24 | describe('environmentManager - input validations', () => { 25 | test('SPLIT_EVALUATOR_ENVIRONMENTS ',async () => { 26 | const environmentManagerFactory = require('../'); 27 | 28 | // Testing environment not string 29 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentNotString; 30 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 31 | 32 | // Testing environment not list 33 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentNotList1; 34 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 35 | 36 | // Testing environment not list 37 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentNotList2; 38 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 39 | 40 | // Testing environment Without AuthToken 41 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentWithoutAuthToken1; 42 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 43 | 44 | // Testing environment Without AuthToken 45 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentWithoutAuthToken2; 46 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 47 | 48 | // Testing environment Without ApiKey 49 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentWithoutApiKey1; 50 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 51 | 52 | // Testing environment Without ApiKey 53 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS=environmentWithoutApiKey2; 54 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 55 | 56 | // Testing environment With Duplicated AuthToken 57 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS= environmentWithDuplicatedAuthToken; 58 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 59 | 60 | // Testing environment With AuthToken Not String 61 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS= environmentWithAuthTokenNotString1; 62 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 63 | 64 | // Testing environment With AuthToken Not String 65 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS= environmentWithAuthTokenNotString2; 66 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 67 | 68 | // Testing environment With AuthToken Not String 69 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS= environmentWithAuthTokenNotString3; 70 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 71 | 72 | // Testing environment With AuthToken Empty 73 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS= environmentWithAuthTokenEmpty; 74 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 75 | 76 | // Testing environment With AuthToken Empty 77 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS= validEnvironment; 78 | expect(() => environmentManagerFactory.getInstance()).not.toThrow(); 79 | await environmentManagerFactory.destroy(); 80 | 81 | }); 82 | 83 | test('Flag sets validation', async () => { 84 | 85 | const evaluateFlagSets = async (greenQuery, redQuery) => { 86 | const environmentManagerFactory = require('../'); 87 | expect(environmentManagerFactory.hasInstance()).toBe(false); 88 | let environmentManager = environmentManagerFactory.getInstance(); 89 | 90 | let environmentSettings = environmentManager.getFactory('key_green').settings; 91 | let queryString = environmentSettings.sync.__splitFiltersValidation.queryString; 92 | expect(queryString).toStrictEqual(greenQuery); 93 | 94 | environmentSettings = environmentManager.getFactory('key_red').settings; 95 | queryString = environmentSettings.sync.__splitFiltersValidation.queryString; 96 | expect(queryString).toStrictEqual(redQuery); 97 | await environmentManager.destroy(); 98 | await environmentManagerFactory.destroy(); 99 | }; 100 | 101 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithValidFlagSets; 102 | await evaluateFlagSets('&sets=set_a,set_b',null); 103 | 104 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentsWithValidFlagSets1; 105 | await evaluateFlagSets('&sets=set_a','&sets=set_x'); 106 | 107 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentsWithValidFlagSets2; 108 | await evaluateFlagSets('&sets=set_1','&sets=set_c'); 109 | 110 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithInvalidFlagSets1; 111 | await evaluateFlagSets('&sets=set_3',null); 112 | 113 | const environmentManagerFactory = require('../'); 114 | 115 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithInvalidFlagSets2; 116 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 117 | expect(environmentManagerFactory.hasInstance()).toBe(false); 118 | 119 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = environmentWithInvalidFlagSets3; 120 | expect(() => environmentManagerFactory.getInstance()).toThrow(); 121 | expect(environmentManagerFactory.hasInstance()).toBe(false); 122 | 123 | }); 124 | }); -------------------------------------------------------------------------------- /environmentManager/index.js: -------------------------------------------------------------------------------- 1 | const settings = require('../utils/parserConfigs')(); 2 | const { validEnvironment, validEnvironmentConfig, isString, throwError, validFlagSets } = require('../utils/parserConfigs/validators'); 3 | const { getSplitFactory } = require('../sdk'); 4 | const { obfuscate } = require('../utils/utils'); 5 | const SPLIT_EVALUATOR_ENVIRONMENTS = 'SPLIT_EVALUATOR_ENVIRONMENTS'; 6 | const SPLIT_EVALUATOR_AUTH_TOKEN = 'SPLIT_EVALUATOR_AUTH_TOKEN'; 7 | const SPLIT_EVALUATOR_API_KEY = 'SPLIT_EVALUATOR_API_KEY'; 8 | const DEFAULT_AUTH_TOKEN = 'DEFAULT_AUTH_TOKEN'; 9 | 10 | const EnvironmentManagerFactory = (function(){ 11 | /** 12 | * EnvironmentManager singleton 13 | */ 14 | class EnvironmentManager { 15 | 16 | constructor() { 17 | // Contains every environment related to an authToken with its apiKey, factory and clientReadiness status 18 | this._environments = {}; 19 | 20 | // Ready promises for each client in environment manager 21 | this._readyPromises = []; 22 | 23 | // Used to know if there is any client ready and evaluator should start 24 | this._clientsReady = false; 25 | 26 | // Defines if openApi security tag should be added 27 | this.requireAuth = true; 28 | 29 | this._initializeEnvironments(); 30 | } 31 | 32 | _initializeEnvironments(){ 33 | 34 | let defaultEnvironment = false; 35 | // If environments envVar is not defined, it creates an environment with auth_token and api_key envVars 36 | if (!process.env.SPLIT_EVALUATOR_ENVIRONMENTS) { 37 | defaultEnvironment = true; 38 | const AUTH_TOKEN = process.env.SPLIT_EVALUATOR_AUTH_TOKEN; 39 | // If auth_token envVar is not defined, means that openapi security tag should not be added 40 | if (!AUTH_TOKEN) { 41 | this.requireAuth = false; 42 | process.env.SPLIT_EVALUATOR_AUTH_TOKEN = DEFAULT_AUTH_TOKEN; 43 | } 44 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS = `[{ 45 | "AUTH_TOKEN": "${process.env[SPLIT_EVALUATOR_AUTH_TOKEN]}", 46 | "API_KEY": "${process.env[SPLIT_EVALUATOR_API_KEY]}" 47 | }]`; 48 | } 49 | 50 | const environmentConfigs = validEnvironmentConfig(SPLIT_EVALUATOR_ENVIRONMENTS); 51 | environmentConfigs.forEach(environment => { 52 | 53 | validEnvironment(environment); 54 | const authToken = environment['AUTH_TOKEN']; 55 | const apiKey = environment['API_KEY']; 56 | settings.core.authorizationKey = apiKey; 57 | 58 | if(!isString(authToken)) { 59 | throwError(`authToken value ${authToken} must be a string value`); 60 | } 61 | 62 | if (this._environments[authToken]) { 63 | throwError(`There are two or more environments with the same authToken '${authToken}' `); 64 | } 65 | 66 | if (!defaultEnvironment) { 67 | const flagSets = validFlagSets(environment['FLAG_SET_FILTER']); 68 | settings.sync = { 69 | ...settings.sync, 70 | splitFilters: flagSets, 71 | }; 72 | } 73 | 74 | const { factory, telemetry, impressionsMode} = getSplitFactory(settings); 75 | 76 | // Creates an environment for authToken 77 | this._environments[authToken] = { 78 | apiKey: apiKey, 79 | factory: factory, 80 | isClientReady: false, 81 | telemetry: telemetry, 82 | impressionsMode: impressionsMode, 83 | lastEvaluation: undefined, 84 | }; 85 | 86 | this._clientReadiness(authToken, apiKey); 87 | }); 88 | } 89 | 90 | _clientReadiness(authToken, apiKey){ 91 | const client = this.getFactory(authToken).client(); 92 | // Add client ready promise to array to wait asynchronously to be resolved 93 | this._readyPromises.push(client.ready()); 94 | // Encode apiKey to log it without exposing it (like ####1234) 95 | const encodedApiKey = obfuscate(apiKey); 96 | // Handle client ready 97 | client.on(client.Event.SDK_READY, () => { 98 | console.info(`Client ready for api key ${encodedApiKey}`); 99 | this._clientsReady = true; 100 | client.isClientReady = true; 101 | this._environments[authToken].isClientReady = true; 102 | }); 103 | // Handle client timed out 104 | client.on(client.Event.SDK_READY_TIMED_OUT, () => { 105 | console.info(`Client timed out for api key ${encodedApiKey}`); 106 | client.isClientReady = false; 107 | this._environments[authToken].isClientReady = false; 108 | client.destroy().then(() => { 109 | console.info('Timed out client destroyed'); 110 | }); 111 | }); 112 | } 113 | 114 | getEnvironment(authToken) { 115 | return this._environments[authToken]; 116 | } 117 | 118 | getFactory(authToken) { 119 | if (!this.requireAuth) authToken = DEFAULT_AUTH_TOKEN; 120 | return this._environments[authToken].factory; 121 | } 122 | 123 | getVersion() { 124 | return this.getFactory(this.getAuthTokens()[0]).settings.sdkVersion; 125 | } 126 | 127 | getClient(authToken) { 128 | return this.getFactory(authToken).client(); 129 | } 130 | 131 | getManager(authToken) { 132 | return this.getFactory(authToken).manager(); 133 | } 134 | 135 | getTelemetry(authToken) { 136 | if (!this.requireAuth) authToken = DEFAULT_AUTH_TOKEN; 137 | const environment = this.getEnvironment(authToken); 138 | const telemetry = environment.telemetry; 139 | const stats = { 140 | splitCount: telemetry ? telemetry.splits.getSplitNames().length : 0, 141 | segmentCount: telemetry ? telemetry.segments.getRegisteredSegments().length : 0, 142 | lastSynchronization: telemetry ? this._reword(telemetry.getLastSynchronization()) : {}, 143 | timeUntilReady: telemetry ? telemetry.getTimeUntilReady() : 0, 144 | httpErrors: telemetry && telemetry.httpErrors ? this._reword(telemetry.httpErrors) : {}, 145 | ready: environment.isClientReady, 146 | impressionsMode: environment.impressionsMode, 147 | lastEvaluation: environment.lastEvaluation, 148 | }; 149 | return stats; 150 | } 151 | 152 | _reword({sp, se, ms, im, ic, ev, te, to}) { 153 | return { 154 | splits: sp, 155 | segments: se, 156 | mySegments: ms, 157 | impressions: im, 158 | impressionCount: ic, 159 | events: ev, 160 | telemetry: te, 161 | token: to, 162 | }; 163 | } 164 | 165 | updateLastEvaluation(authToken) { 166 | if (!this.requireAuth) authToken = DEFAULT_AUTH_TOKEN; 167 | this._environments[authToken].lastEvaluation = new Date().toJSON(); 168 | } 169 | 170 | validToken(authToken) { 171 | if (this.requireAuth) return Object.keys(this._environments).indexOf(authToken) > -1; 172 | return true; 173 | } 174 | 175 | getAuthTokens() { 176 | return Object.keys(this._environments); 177 | } 178 | 179 | // Awaits for every client ready promise 180 | async ready() { 181 | return Promise.allSettled(this._readyPromises).then((environmentsStatus) => { 182 | this._clientsReady = environmentsStatus.some(environment => environment.status === 'fulfilled'); 183 | return this._clientsReady; 184 | }); 185 | } 186 | 187 | async destroy() { 188 | return Promise.all( 189 | this.getAuthTokens().map(authToken => { 190 | const client = this.getClient(authToken); 191 | return client.destroy(); 192 | }) 193 | ).then(() => { 194 | this._clientsReady = false; 195 | this._environments = []; 196 | }); 197 | } 198 | 199 | isReady() { 200 | return this._clientsReady; 201 | } 202 | } 203 | 204 | let instance; 205 | 206 | // Singleton handle strategy 207 | return { 208 | hasInstance() { 209 | return !instance ? false : true; 210 | }, 211 | getInstance() { 212 | if (!instance) { 213 | instance = new EnvironmentManager(); 214 | } 215 | return instance; 216 | }, 217 | async destroy() { 218 | if (!instance) return; 219 | await instance.destroy().then(() => { instance = undefined; }); 220 | }, 221 | }; 222 | })(); 223 | 224 | module.exports = EnvironmentManagerFactory; -------------------------------------------------------------------------------- /listener/__tests__/ip-addresses.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { expectOk, gracefulShutDown } = require('../../utils/testWrapper/index'); 3 | 4 | describe('ip addresses', () => { 5 | let ip; 6 | let hostname; 7 | 8 | const log = (impressionData) => { 9 | ip = impressionData.ip; 10 | hostname = impressionData.hostname; 11 | }; 12 | 13 | const mockListener = () => { 14 | const environmentManager = require('../../environmentManager').getInstance(); 15 | environmentManager.getAuthTokens().forEach(authToken => { 16 | environmentManager.getFactory(authToken).settings.impressionListener.logImpression = log; 17 | }); 18 | }; 19 | 20 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = 'http://localhost:7546'; 21 | 22 | beforeEach(() => { 23 | jest.resetModules(); 24 | }); 25 | 26 | afterEach(async () => { 27 | await gracefulShutDown(); 28 | }); 29 | 30 | describe('ip addresses default', () => { 31 | test('should set hostname and ip when is default', async () => { 32 | const app = require('../../app'); 33 | const os = require('os'); 34 | const localIp = require('@splitsoftware/splitio/cjs/utils/ip'); 35 | mockListener(); 36 | const response = await request(app) 37 | .get('/client/get-treatment?key=test&split-name=my-experiment') 38 | .set('Authorization', 'test'); 39 | expectOk(response, 200, 'on', 'my-experiment'); 40 | await new Promise(resolve => setTimeout(resolve, 100)); 41 | expect(ip).toBe(localIp.address()); 42 | expect(hostname).toBe(os.hostname()); 43 | }); 44 | }); 45 | 46 | describe('ip addresses disabled', () => { 47 | beforeEach(() => { 48 | process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED = 'false'; 49 | }); 50 | test('should not set hostname and ip when is false', async () => { 51 | const app = require('../../app'); 52 | 53 | mockListener(); 54 | 55 | const response = await request(app) 56 | .get('/client/get-treatment?key=test&split-name=my-experiment') 57 | .set('Authorization', 'test'); 58 | expectOk(response, 200, 'on', 'my-experiment'); 59 | await new Promise(resolve => setTimeout(resolve, 100)); 60 | expect(ip).toBe(false); 61 | expect(hostname).toBe(false); 62 | }); 63 | }); 64 | 65 | describe('ip addresses enabled', () => { 66 | beforeEach(() => { 67 | process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED = 'true'; 68 | }); 69 | test('should set hostname and ip when is true', async () => { 70 | const app = require('../../app'); 71 | const os = require('os'); 72 | const localIp = require('@splitsoftware/splitio/cjs/utils/ip'); 73 | mockListener(); 74 | 75 | const response = await request(app) 76 | .get('/client/get-treatment?key=test&split-name=my-experiment') 77 | .set('Authorization', 'test'); 78 | expectOk(response, 200, 'on', 'my-experiment'); 79 | await new Promise(resolve => setTimeout(resolve, 100)); 80 | expect(ip).toBe(localIp.address()); 81 | expect(hostname).toBe(os.hostname()); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /listener/__tests__/listener.test.js: -------------------------------------------------------------------------------- 1 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = 'http://localhost:7546'; 2 | 3 | const http = require('http'); 4 | const request = require('supertest'); 5 | const app = require('../../app'); 6 | const { expectOk, expectOkMultipleResults } = require('../../utils/testWrapper/index'); 7 | 8 | /** 9 | * matcherIlRequest matches the request body with paramaters passed 10 | * @param {string} body 11 | * @param {number} length 12 | * @param {array} expectedSplits 13 | */ 14 | const matcherIlRequest = (body, length, expectedSplits) => { 15 | const ilRequest = JSON.parse(body); 16 | expect(ilRequest).toHaveProperty('impressions'); 17 | const impressions = ilRequest.impressions; 18 | expect(impressions.length).toEqual(length); 19 | const parsed = {}; 20 | impressions.forEach(impression => { 21 | parsed[impression.testName] = impression.keyImpressions; 22 | }); 23 | expectedSplits.forEach(expected => { 24 | expect(parsed).toHaveProperty(expected.split); 25 | expect(parsed[expected.split].length).toEqual(expected.length); 26 | }); 27 | }; 28 | 29 | describe('impression-listener', () => { 30 | let server; 31 | let body = ''; 32 | 33 | beforeAll(done => { 34 | // Create a server simulating a place to POST impressions when an IL is attached 35 | server = http.createServer((req, res) => { 36 | if (req.method === 'POST') { 37 | req.on('data', data => { 38 | body += data; 39 | }); 40 | req.on('end', () => { 41 | res.write('ok'); 42 | res.end(); 43 | return; 44 | }); 45 | } else { 46 | res.write('ok'); 47 | res.end(); 48 | } 49 | }); 50 | server.listen(7546, done); 51 | }); 52 | 53 | test('should have 5 impressions sent by max impressions to post', async () => { 54 | const t0 = new Date().getTime(); 55 | // Generates the max size of impressions to be sent 56 | let response = await request(app) 57 | .get('/client/get-treatment?key=test&split-name=my-experiment') 58 | .set('Authorization', 'test'); 59 | expectOk(response, 200, 'on', 'my-experiment'); 60 | response = await request(app) 61 | .get('/client/get-treatments?key=test&split-names=my-experiment,other-experiment-3,other-experiment-2,other-experiment') 62 | .set('Authorization', 'test'); 63 | expectOkMultipleResults(response, 200, { 64 | 'my-experiment': { treatment: 'on' }, 65 | 'other-experiment-3': { treatment: 'off' }, 66 | 'other-experiment-2': { treatment: 'on' }, 67 | 'other-experiment': { treatment: 'control' }, 68 | }, 4); 69 | // Wait for impression sender task 70 | await new Promise(resolve => setTimeout(resolve, 300)); 71 | // Matches all the impressions in the body of the IL Post Impressions 72 | matcherIlRequest(body, 4, [ 73 | { split: 'my-experiment', length: 2 }, 74 | { split: 'other-experiment-3', length: 1 }, 75 | { split: 'other-experiment', length: 1 }, 76 | { split: 'other-experiment-2', length: 1 } 77 | ]); 78 | body = ''; 79 | const t1 = new Date().getTime(); 80 | expect(t1-t0).toBeLessThanOrEqual(550); 81 | }); 82 | 83 | test('should have 1 impressions sent by time schedule', async () => { 84 | // Generates one impression and just wait until is sent by scheduler (1 second for testing) 85 | const response = await request(app) 86 | .get('/client/get-treatment?key=test&split-name=my-experiment') 87 | .set('Authorization', 'test'); 88 | expectOk(response, 200, 'on', 'my-experiment'); 89 | 90 | // Wait for impression sender task 91 | await new Promise(resolve => setTimeout(resolve, 700)); 92 | // Matches the impression in the body of the IL Post Impressions 93 | matcherIlRequest(body, 1, [{ split: 'my-experiment', length: 1 }]); 94 | body = ''; 95 | }); 96 | 97 | afterAll(done => { 98 | server.close(done); 99 | }); 100 | }); -------------------------------------------------------------------------------- /listener/__tests__/queue.test.js: -------------------------------------------------------------------------------- 1 | const ImpressionQueue = require('../queue'); 2 | 3 | const impression1 = { 4 | keyName: 'keyName', 5 | feature: 'feature1', 6 | treatment: 'treatment', 7 | time: 123456789, 8 | changeNumber: 123456789, 9 | label: 'label', 10 | }; 11 | 12 | const impression2 = { 13 | keyName: 'keyName2', 14 | feature: 'feature2', 15 | treatment: 'feature2', 16 | time: 987654321, 17 | changeNumber: 987654321, 18 | label: 'label2', 19 | }; 20 | 21 | describe('impression queue', () => { 22 | // Test behavior when impressions are added 23 | test('test add/size', done => { 24 | const impressionQueue = new ImpressionQueue(); 25 | expect(impressionQueue.getSize()).toEqual(0); 26 | impressionQueue.addImpression(impression1); 27 | expect(impressionQueue.getSize()).toEqual(1); 28 | done(); 29 | }); 30 | 31 | // Test wrapper schema to send impressions, it should be wrapped by feature 32 | test('test impressionToPost', done => { 33 | const impressionQueue = new ImpressionQueue(); 34 | impressionQueue.addImpression(impression1); 35 | impressionQueue.addImpression(impression2); 36 | impressionQueue.addImpression(impression1); 37 | expect(impressionQueue.getSize()).toEqual(3); 38 | 39 | const result = impressionQueue.getImpressionsToPost(); 40 | expect(impressionQueue.getSize()).toEqual(0); 41 | expect(result.length).toEqual(2); 42 | 43 | expect(result[0]).toHaveProperty('testName', 'feature1'); 44 | expect(result[0]).toHaveProperty('keyImpressions'); 45 | expect(result[0].keyImpressions.length).toEqual(2); 46 | const cloned1 = Object.assign({}, impression1); 47 | delete cloned1.feature; 48 | expect(result[0].keyImpressions).toEqual(expect.arrayContaining([cloned1, cloned1])); 49 | 50 | expect(result[1]).toHaveProperty('testName', 'feature2'); 51 | expect(result[1]).toHaveProperty('keyImpressions'); 52 | expect(result[1].keyImpressions.length).toEqual(1); 53 | const cloned2 = Object.assign({}, impression2); 54 | delete cloned2.feature; 55 | expect(result[1].keyImpressions).toEqual(expect.arrayContaining([cloned2])); 56 | done(); 57 | }); 58 | }); -------------------------------------------------------------------------------- /listener/index.js: -------------------------------------------------------------------------------- 1 | const impressionManager = require('./manager').getInstance(); 2 | 3 | /** 4 | * logImpression impresion listener handler 5 | * @param {Object} impressionData 6 | */ 7 | const logImpression = (impressionData) => { 8 | // Adds Impression to queue 9 | impressionManager.trackImpression(impressionData.impression); 10 | }; 11 | 12 | module.exports = { 13 | logImpression, 14 | }; -------------------------------------------------------------------------------- /listener/manager.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const config = require('config'); 3 | const repeat = require('./repeat'); 4 | const ImpressionQueue = require('./queue'); 5 | 6 | const IMPRESSIONS_PER_POST = config.get('impressionsPerPost') ? config.get('impressionsPerPost') : 500; 7 | const IMPRESSION_SEND_RATE = config.get('impressionsSendRate') ? config.get('impressionsSendRate') : 30000; 8 | 9 | // ImpressionManager is in charge of creating the ImpressionQueue to store impressions for listener and starts 10 | // the task that will flush impressions every XXX seconds configured in sdk configs. If at some point the queue 11 | // reaches the max size, it will execute an explicit flush of the impressions accumulated. 12 | const ImpressionManagerFactory = (function(){ 13 | class ImpressionManager { 14 | constructor() { 15 | this._stopImpressionSender = false; 16 | this._impressionQueue = new ImpressionQueue(); 17 | this._startImpressionsSender(); 18 | } 19 | 20 | static postImpressions(impressions) { 21 | const url = process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT; 22 | const options = { 23 | method: 'POST', 24 | body: JSON.stringify({ impressions }), 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | json: true, 29 | }; 30 | return (impressions.length > 0) ? fetch(url, options) 31 | .catch(error => { 32 | console.log(error && error.message); 33 | return Promise.reject(error); 34 | }) : Promise.resolve(); 35 | } 36 | 37 | _startImpressionsSender() { 38 | this._stopImpressionSender = repeat( 39 | schedulePublisher => ImpressionManager.postImpressions(this._impressionQueue.getImpressionsToPost()).then(() => schedulePublisher()), 40 | IMPRESSION_SEND_RATE 41 | ); 42 | } 43 | 44 | // This will be used every time that the max amount of impressions is reached 45 | _flushAndResetTime() { 46 | ImpressionManager.postImpressions(this._impressionQueue.getImpressionsToPost()); 47 | this._stopImpressionSender.reset(); 48 | } 49 | 50 | trackImpression(impression) { 51 | this._impressionQueue.addImpression(impression); 52 | 53 | // Flushes only if is greater than equal IMPRESSIONS_PER_POST 54 | if (this._impressionQueue.getSize() >= IMPRESSIONS_PER_POST) { 55 | this._flushAndResetTime(); 56 | } 57 | } 58 | 59 | destroy(){ 60 | this._stopImpressionSender(); 61 | } 62 | } 63 | 64 | let instance; 65 | 66 | return { 67 | hasInstance() { 68 | return !instance ? false : true; 69 | }, 70 | getInstance() { 71 | if (!instance) { 72 | instance = new ImpressionManager(); 73 | } 74 | return instance; 75 | }, 76 | async destroy() { 77 | if (!instance) return; 78 | await instance.destroy(); 79 | instance = undefined; 80 | }, 81 | }; 82 | })(); 83 | 84 | module.exports = ImpressionManagerFactory; -------------------------------------------------------------------------------- /listener/queue.js: -------------------------------------------------------------------------------- 1 | const config = require('config'); 2 | const IMPRESSIONS_PER_POST = config.get('impressionsPerPost') ? config.get('impressionsPerPost') : 500; 3 | 4 | class ImpressionQueue { 5 | constructor() { 6 | this._impressions = []; 7 | } 8 | 9 | /** 10 | * addImpression adds one impression into queue 11 | * @param {Object} impression 12 | */ 13 | addImpression(impression) { 14 | this._impressions.push(impression); 15 | } 16 | 17 | /** 18 | * getSize returns the current size of the queue 19 | */ 20 | getSize() { 21 | return this._impressions.length; 22 | } 23 | 24 | /** 25 | * getImpressionsToPost generates the impressions object to be sent 26 | */ 27 | getImpressionsToPost() { 28 | const impressionsToPost = []; 29 | const splice = this._impressions.splice(0, IMPRESSIONS_PER_POST); 30 | const groupedImpressions = new Map(); 31 | 32 | splice.forEach(impression => { 33 | // Builds the keyImpression object 34 | const keyImpressions = { 35 | keyName: impression.keyName, 36 | treatment: impression.treatment, 37 | time: impression.time, 38 | changeNumber: impression.changeNumber, 39 | label: impression.label, 40 | }; 41 | // Checks if already exists in order to add o create new impression bulks 42 | if (!groupedImpressions.has(impression.feature)) { 43 | groupedImpressions.set(impression.feature, [keyImpressions]); 44 | } else { 45 | const currentImpressions = groupedImpressions.get(impression.feature); 46 | groupedImpressions.set(impression.feature, currentImpressions.concat(keyImpressions)); 47 | } 48 | }); 49 | 50 | // Builds the entire bulk 51 | /* 52 | { 53 | "impressions": [{ 54 | "testName": "example-1", 55 | "keyImpressions": [impression1, impression2, ...] 56 | }, { 57 | "testName": "example-1", 58 | "keyImpressions": [impression1, impression2, ...] 59 | }] 60 | } 61 | */ 62 | groupedImpressions.forEach((value, key) => { 63 | const impressionToPost = { 64 | testName: key, 65 | keyImpressions: value, 66 | }; 67 | impressionsToPost.push(impressionToPost); 68 | }); 69 | return impressionsToPost; 70 | } 71 | } 72 | module.exports = ImpressionQueue; -------------------------------------------------------------------------------- /listener/repeat.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2016 Split Software 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | **/ 16 | 17 | function repeat(fn, delay, ...rest) { 18 | let tid; 19 | let stopped = false; 20 | 21 | function next(_delay = delay, ...rest) { 22 | if (!stopped) { 23 | tid = setTimeout(() => { 24 | fn(...rest, next); 25 | }, _delay); 26 | } 27 | } 28 | 29 | function till() { 30 | clearTimeout(tid); 31 | tid = undefined; 32 | stopped = true; 33 | } 34 | 35 | till.reset = () => { 36 | clearTimeout(tid); 37 | tid = undefined; 38 | next(delay, ...rest); 39 | }; 40 | 41 | fn(...rest, next); 42 | 43 | return till; 44 | } 45 | 46 | module.exports = repeat; -------------------------------------------------------------------------------- /manager/__tests__/split.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | const { expectError, expectErrorContaining } = require('../../utils/testWrapper'); 4 | 5 | describe('split', () => { 6 | // Testing authorization 7 | test('should be 401 if auth is not passed', async () => { 8 | const response = await request(app) 9 | .get('/manager/split?split-name=split'); 10 | expectError(response, 401, 'Unauthorized'); 11 | }); 12 | 13 | test('should be 401 if auth does not match', async () => { 14 | const response = await request(app) 15 | .get('/manager/split?split-name=split') 16 | .set('Authorization', 'invalid'); 17 | expectError(response, 401, 'Unauthorized'); 18 | }); 19 | 20 | test('should be 400 if split-name is not passed', async () => { 21 | const expected = [ 22 | 'you passed a null or undefined split-name, split-name must be a non-empty string.' 23 | ]; 24 | const response = await request(app) 25 | .get('/manager/split') 26 | .set('Authorization', 'test'); 27 | expectErrorContaining(response, 400, expected); 28 | }); 29 | 30 | test('should be 400 if split-name is empty', async () => { 31 | const expected = [ 32 | 'you passed an empty split-name, split-name must be a non-empty string.' 33 | ]; 34 | const response = await request(app) 35 | .get('/manager/split?split-name=') 36 | .set('Authorization', 'test'); 37 | expectErrorContaining(response, 400, expected); 38 | }); 39 | 40 | test('should be 400 if split-name is empty trimmed', async () => { 41 | const expected = [ 42 | 'you passed an empty split-name, split-name must be a non-empty string.' 43 | ]; 44 | const response = await request(app) 45 | .get('/manager/split?split-name= ') 46 | .set('Authorization', 'test'); 47 | expectErrorContaining(response, 400, expected); 48 | }); 49 | 50 | test('should be 404 if split is not found', async () => { 51 | const response = await request(app) 52 | .get('/manager/split?split-name=not-found') 53 | .set('Authorization', 'test'); 54 | expect(response.statusCode).toBe(404); 55 | expect(response.body).toHaveProperty('error', 'Feature flag "not-found" was not found.'); 56 | }); 57 | 58 | test('should be 200 and matches with passed split in YAML', async () => { 59 | const response = await request(app) 60 | .get('/manager/split?split-name=my-experiment') 61 | .set('Authorization', 'test'); 62 | expect(response.statusCode).toBe(200); 63 | expect(response.body).toHaveProperty('name', 'my-experiment'); 64 | expect(response.body).toHaveProperty('trafficType', 'localhost'); 65 | expect(response.body).toHaveProperty('killed', false); 66 | expect(response.body).toHaveProperty('changeNumber', 0); 67 | expect(response.body).toHaveProperty('treatments'); 68 | expect(response.body.treatments).toEqual(expect.arrayContaining(['on', 'off'])); 69 | expect(response.body).toHaveProperty('configs'); 70 | expect(response.body.configs).toEqual({ 71 | on: '{"desc" : "this applies only to ON treatment"}', 72 | off: '{"desc" : "this applies only to OFF and only for only_test. The rest will receive ON"}', 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /manager/__tests__/splitNames.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | const { expectError } = require('../../utils/testWrapper'); 4 | 5 | describe('names', () => { 6 | // Testing authorization 7 | test('should be 401 if auth is not passed', async () => { 8 | const response = await request(app) 9 | .get('/manager/names'); 10 | expectError(response, 401, 'Unauthorized'); 11 | }); 12 | 13 | test('should be 401 if auth does not match', async () => { 14 | const response = await request(app) 15 | .get('/manager/names') 16 | .set('Authorization', 'invalid'); 17 | expectError(response, 401, 'Unauthorized'); 18 | }); 19 | 20 | test('should be 200 and retuns the feature flags defined in YAML', async () => { 21 | const response = await request(app) 22 | .get('/manager/names') 23 | .set('Authorization', 'test'); 24 | expect(response.statusCode).toBe(200); 25 | expect(response.body).toHaveProperty('splits'); 26 | expect(response.body.splits.length).toEqual(4); 27 | expect(response.body.splits) 28 | .toEqual(expect.arrayContaining(['my-experiment', 'other-experiment-3', 'other-experiment', 'other-experiment-2'])); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /manager/__tests__/splits.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const app = require('../../app'); 3 | const { expectError } = require('../../utils/testWrapper'); 4 | 5 | describe('splits', () => { 6 | // Testing authorization 7 | test('should be 401 if auth is not passed', async () => { 8 | const response = await request(app) 9 | .get('/manager/splits'); 10 | expectError(response, 401, 'Unauthorized'); 11 | }); 12 | 13 | test('should be 401 if auth does not match', async () => { 14 | const response = await request(app) 15 | .get('/manager/splits') 16 | .set('Authorization', 'invalid'); 17 | expectError(response, 401, 'Unauthorized'); 18 | }); 19 | 20 | test('should be 200 and returns the feature flags added in YAML', async () => { 21 | const response = await request(app) 22 | .get('/manager/splits') 23 | .set('Authorization', 'test'); 24 | expect(response.statusCode).toBe(200); 25 | expect(response.body).toHaveProperty('splits'); 26 | const keys = Object.keys(response.body.splits); 27 | expect(keys.length).toEqual(4); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /manager/manager.controller.js: -------------------------------------------------------------------------------- 1 | const environmentManager = require('../environmentManager').getInstance(); 2 | 3 | /** 4 | * split returns splitView for a particular feature flag 5 | * @param {*} req 6 | * @param {*} res 7 | */ 8 | const split = async (req, res) => { 9 | const featureFlagName = req.splitio.featureFlagName; 10 | 11 | try { 12 | const manager = environmentManager.getManager(req.headers.authorization); 13 | const featureFlag = await manager.split(featureFlagName); 14 | return featureFlag ? res.send(featureFlag) : res.status(404).send({ 15 | error: `Feature flag "${featureFlagName}" was not found.`, 16 | }); 17 | } catch (error) { 18 | res.status(500).send({error}); 19 | } 20 | }; 21 | 22 | /** 23 | * splits returns featureFlags 24 | * @param {*} req 25 | * @param {*} res 26 | */ 27 | const splits = async (req, res) => { 28 | try { 29 | const manager = environmentManager.getManager(req.headers.authorization); 30 | const featureFlags = await manager.splits(); 31 | res.send({ 32 | splits: featureFlags, 33 | }); 34 | } catch (error) { 35 | res.status(500).send({error}); 36 | } 37 | }; 38 | 39 | /** 40 | * splitNames returns featureFlagNames 41 | * @param {*} req 42 | * @param {*} res 43 | */ 44 | const splitNames = async (req, res) => { 45 | try { 46 | const manager = environmentManager.getManager(req.headers.authorization); 47 | const featureFlagNames = await manager.names(); 48 | res.send({ 49 | splits: featureFlagNames, 50 | }); 51 | } catch (error) { 52 | res.status(500).send({error}); 53 | } 54 | }; 55 | 56 | module.exports = { 57 | splitNames, 58 | split, 59 | splits, 60 | }; 61 | -------------------------------------------------------------------------------- /manager/manager.router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const splitValidator = require('../utils/inputValidation/split'); 4 | const managerController = require('./manager.controller'); 5 | const { parseValidators } = require('../utils/utils'); 6 | 7 | /** 8 | * featureFlagValidation performs input validation for manager call 9 | * @param {object} req 10 | * @param {object} res 11 | * @param {function} next 12 | */ 13 | const featureFlagValidation = (req, res, next) => { 14 | const featureFlagNameValidation = splitValidator(req.query['split-name']); 15 | 16 | const error = parseValidators([featureFlagNameValidation]); 17 | if (error.length) { 18 | return res 19 | .status(400) 20 | .send({ 21 | error, 22 | }); 23 | } else { 24 | req.splitio = { 25 | featureFlagName: featureFlagNameValidation.value, 26 | }; 27 | } 28 | 29 | next(); 30 | }; 31 | 32 | router.get('/split', featureFlagValidation, managerController.split); 33 | router.get('/splits', managerController.splits); 34 | router.get('/names', managerController.splitNames); 35 | 36 | module.exports = router; 37 | -------------------------------------------------------------------------------- /middleware/authorization.js: -------------------------------------------------------------------------------- 1 | const environmentManager = require('../environmentManager').getInstance(); 2 | 3 | /** 4 | * authorization checks if AUTH_TOKEN matches with the one passed 5 | * as header 6 | * @param {*} req 7 | * @param {*} res 8 | * @param {*} next 9 | */ 10 | const authorization = (req, res, next) => { 11 | if (environmentManager.validToken(req.headers.authorization)) { 12 | next(); 13 | } else { 14 | console.log('Returning 401 Unauthorized.'); 15 | res.status(401).send({ 16 | error: 'Unauthorized', 17 | }); 18 | } 19 | }; 20 | 21 | module.exports = authorization; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split-evaluator", 3 | "version": "2.7.0", 4 | "description": "Split-Evaluator", 5 | "repository": "splitio/split-evaluator", 6 | "homepage": "https://github.com/splitio/split-evaluator#readme", 7 | "bugs": "https://github.com/splitio/split-evaluator/issues", 8 | "license": "Apache-2.0", 9 | "author": "Facundo Cabrera ", 10 | "contributors": [ 11 | { 12 | "name": "Nico Zelaya", 13 | "email": "nicolas.zelaya@split.io", 14 | "url": "https://github.com/NicoZelaya" 15 | }, 16 | { 17 | "name": "Matias Melograno", 18 | "email": "matias.melograno@split.io", 19 | "url": "https://github.com/mmelograno" 20 | } 21 | ], 22 | "jest": { 23 | "testPathIgnorePatterns": [ 24 | "config", 25 | "environmentManager/__tests__/constants" 26 | ], 27 | "setupFiles": [ 28 | "/.jest/setEnvVars.js" 29 | ], 30 | "setupFilesAfterEnv": [ 31 | "/.jest/app.setup.js" 32 | ] 33 | }, 34 | "main": "server.js", 35 | "scripts": { 36 | "dev": "nodemon server.js", 37 | "start": "node server.js", 38 | "start:debug": "node --inspect-brk server.js", 39 | "lint": "eslint .", 40 | "test": "NODE_ENV=test jest" 41 | }, 42 | "dependencies": { 43 | "@splitsoftware/splitio": "11.0.3", 44 | "config": "^3.3.9", 45 | "express": "^4.17.1", 46 | "morgan": "^1.9.1", 47 | "swagger-ui-express": "^4.3.0" 48 | }, 49 | "engines": { 50 | "node": ">=16" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.15.5", 54 | "@babel/preset-env": "^7.15.6", 55 | "babel-jest": "^29.7.0", 56 | "eslint": "^8.9.0", 57 | "jest": "^29.7.0", 58 | "nodemon": "^3.1.0", 59 | "superagent": "^8.0.9", 60 | "supertest": "^6.3.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sdk.js: -------------------------------------------------------------------------------- 1 | // 2 | // SDK initialization and factory instanciation. 3 | // 4 | const SplitFactory = require('@splitsoftware/splitio').SplitFactory; 5 | const utils = require('./utils/utils'); 6 | 7 | const getSplitFactory = (settings) => { 8 | const logLevel = settings.logLevel; 9 | delete settings.logLevel; 10 | 11 | let impressionsMode; 12 | let telemetry; 13 | const factory = SplitFactory(settings, (modules) => { 14 | // Do not try this at home. 15 | modules.settings.sdkVersion = modules.settings.version; 16 | modules.settings.version = `evaluator-${utils.getVersion()}`; 17 | impressionsMode = modules.settings.sync.impressionsMode; 18 | const originalStorageFactory = modules.storageFactory; 19 | modules.storageFactory = (config) => { 20 | const storage = originalStorageFactory(config); 21 | telemetry = storage.telemetry; 22 | return storage; 23 | }; 24 | }); 25 | 26 | if (logLevel) { 27 | console.log('Setting log level with', logLevel); 28 | factory.Logger.setLogLevel(logLevel); 29 | } 30 | 31 | return { factory, telemetry, impressionsMode }; 32 | }; 33 | 34 | module.exports = { 35 | getSplitFactory, 36 | }; 37 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const config = require('config'); 2 | const utils = require('./utils/utils'); 3 | const environmentManagerFactory = require('./environmentManager'); 4 | const environmentManager = environmentManagerFactory.getInstance(); 5 | const impressionManagerFactory = require('./listener/manager'); 6 | 7 | const app = require('./app'); 8 | 9 | const PORT = process.env.SPLIT_EVALUATOR_SERVER_PORT || 7548; 10 | 11 | let server; 12 | // Only available for in memory settings. 13 | if (config.get('blockUntilReady')) { 14 | environmentManager.ready().then(ready => spinUpServer(ready)); 15 | } else { 16 | environmentManager.ready().then(ready => { 17 | if (!ready) { 18 | console.log('There is no client ready, initialization aborted'); 19 | process.exit(0); 20 | } 21 | }); 22 | spinUpServer(true); 23 | } 24 | 25 | function spinUpServer(splitClientsReady) { 26 | if (!splitClientsReady) { 27 | console.log('There is no client ready, initialization aborted'); 28 | return; 29 | } 30 | server = app.listen(PORT, '0.0.0.0', function () { 31 | utils.uptime('init'); 32 | console.log('Server is Up and Running at Port : ' + PORT); 33 | }); 34 | } 35 | 36 | gracefulShutDown('SIGTERM'); 37 | gracefulShutDown('SIGINT'); 38 | 39 | function gracefulShutDown(signal) { 40 | process.on(signal, async () => { 41 | console.info(`${signal} signal received.`); 42 | await environmentManagerFactory.destroy(); 43 | await impressionManagerFactory.destroy(); 44 | server.close(() => { 45 | console.log('Http server closed.'); 46 | process.exit(0); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; 2 | 3 | const EMPTY_FLAG_SETS = 'you passed an empty flag-sets, flag-sets must be a non-empty array.'; 4 | const NULL_FLAG_SETS = 'you passed a null or undefined flag-sets, flag-sets must be a non-empty array.'; 5 | 6 | module.exports = { 7 | TRIMMABLE_SPACES_REGEX, 8 | EMPTY_FLAG_SETS, 9 | NULL_FLAG_SETS, 10 | }; -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/attributes.test.js: -------------------------------------------------------------------------------- 1 | const attributesValidator = require('../attributes'); 2 | const validAttributesMock = { 'my-attr1':true, 'my-attr2':5, 'my-attr3': 'asd', 'my-attr4': [] }; 3 | 4 | describe('attributes validator', () => { 5 | test('should return error on invalid attributes', done => { 6 | const expected = 'attributes must be a plain object.'; 7 | 8 | const result = attributesValidator('test'); 9 | 10 | expect(result).toHaveProperty('valid', false); 11 | expect(result).toHaveProperty('error', expected); 12 | expect(result).not.toHaveProperty('value'); 13 | done(); 14 | }); 15 | 16 | test('should return error on invalid attributes 2', done => { 17 | const expected = 'attributes must be a plain object.'; 18 | 19 | const result = attributesValidator('[]'); 20 | 21 | expect(result).toHaveProperty('valid', false); 22 | expect(result).toHaveProperty('error', expected); 23 | expect(result).not.toHaveProperty('value'); 24 | done(); 25 | }); 26 | 27 | test('should return error on invalid attributes 3', done => { 28 | const expected = 'attributes must be a plain object.'; 29 | 30 | const result = attributesValidator('true'); 31 | 32 | expect(result).toHaveProperty('valid', false); 33 | expect(result).toHaveProperty('error', expected); 34 | expect(result).not.toHaveProperty('value'); 35 | done(); 36 | }); 37 | 38 | test('should be valid when attributes is an object', done => { 39 | const result = attributesValidator(validAttributesMock); 40 | expect(result).toHaveProperty('valid', true); 41 | expect(result).toHaveProperty('value', { 42 | ...validAttributesMock, 43 | }); 44 | expect(result).not.toHaveProperty('error'); 45 | done(); 46 | }); 47 | 48 | test('should be valid when attributes is a stringified object', done => { 49 | const result = attributesValidator(JSON.stringify(validAttributesMock)); 50 | expect(result).toHaveProperty('valid', true); 51 | expect(result).toHaveProperty('value', { 52 | ...validAttributesMock, 53 | }); 54 | expect(result).not.toHaveProperty('error'); 55 | done(); 56 | }); 57 | 58 | test('should be valid when attributes is empty object', done => { 59 | const result = attributesValidator('{}'); 60 | 61 | expect(result).toHaveProperty('valid', true); 62 | expect(result).toHaveProperty('value', {}); 63 | expect(result).not.toHaveProperty('error'); 64 | done(); 65 | }); 66 | 67 | test('should be valid when attributes is null', done => { 68 | const result = attributesValidator(); 69 | 70 | expect(result).toHaveProperty('valid', true); 71 | expect(result).toHaveProperty('value', null); 72 | expect(result).not.toHaveProperty('error'); 73 | done(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/error.test.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('../wrapper/error'); 2 | 3 | test('should parse messages for error', done => { 4 | const result = errorWrapper('thisIsError'); 5 | expect(result).toHaveProperty('valid', false); 6 | expect(result).toHaveProperty('error', 'thisIsError'); 7 | expect(result).not.toHaveProperty('value'); 8 | done(); 9 | }); -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/eventType.test.js: -------------------------------------------------------------------------------- 1 | const eventTypeValidator = require('../eventType'); 2 | 3 | describe('eventType validator', () => { 4 | test('should return error on undefined', done => { 5 | const expected = 'you passed a null or undefined event-type, event-type must be a non-empty string.'; 6 | 7 | const result = eventTypeValidator(); 8 | 9 | expect(result).toHaveProperty('valid', false); 10 | expect(result).toHaveProperty('error', expected); 11 | expect(result).not.toHaveProperty('value'); 12 | done(); 13 | }); 14 | 15 | test('should return error on empty', done => { 16 | const expected = 'you passed an empty event-type, event-type must be a non-empty string.'; 17 | 18 | const result = eventTypeValidator(''); 19 | 20 | expect(result).toHaveProperty('valid', false); 21 | expect(result).toHaveProperty('error', expected); 22 | expect(result).not.toHaveProperty('value'); 23 | done(); 24 | }); 25 | 26 | test('should return error on not valid event-type', done => { 27 | const expected = 'you passed "@asdasda", event-type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.'; 28 | 29 | const result = eventTypeValidator('@asdasda'); 30 | 31 | expect(result).toHaveProperty('valid', false); 32 | expect(result).toHaveProperty('error', expected); 33 | expect(result).not.toHaveProperty('value'); 34 | done(); 35 | }); 36 | 37 | test('should be valid when ok', done => { 38 | const result = eventTypeValidator('my-event-type'); 39 | 40 | expect(result).toHaveProperty('valid', true); 41 | expect(result).toHaveProperty('value', 'my-event-type'); 42 | expect(result).not.toHaveProperty('error'); 43 | done(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/eventValue.test.js: -------------------------------------------------------------------------------- 1 | const valueValidator = require('../value'); 2 | 3 | describe('value validator', () => { 4 | test('should return error on empty', done => { 5 | const expected = 'value must be null or number.'; 6 | 7 | const result = valueValidator(''); 8 | 9 | expect(result).toHaveProperty('valid', false); 10 | expect(result).toHaveProperty('error', expected); 11 | expect(result).not.toHaveProperty('value'); 12 | done(); 13 | }); 14 | 15 | test('should return error on empty trim', done => { 16 | const expected = 'value must be null or number.'; 17 | 18 | const result = valueValidator(' '); 19 | 20 | expect(result).toHaveProperty('valid', false); 21 | expect(result).toHaveProperty('error', expected); 22 | expect(result).not.toHaveProperty('value'); 23 | done(); 24 | }); 25 | 26 | test('should return valid on undefined', done => { 27 | const result = valueValidator(); 28 | 29 | expect(result).toHaveProperty('valid', true); 30 | expect(result).toHaveProperty('value', null); 31 | expect(result).not.toHaveProperty('error'); 32 | done(); 33 | }); 34 | 35 | test('should return valid on number', done => { 36 | const result = valueValidator('1234'); 37 | 38 | expect(result).toHaveProperty('valid', true); 39 | expect(result).toHaveProperty('value', 1234); 40 | expect(result).not.toHaveProperty('error'); 41 | done(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/key.test.js: -------------------------------------------------------------------------------- 1 | const keyValidator = require('../key'); 2 | const { getLongKey } = require('../../testWrapper'); 3 | 4 | describe('key validator', () => { 5 | test('should return error on undefined', done => { 6 | const expected = 'you passed a null or undefined key, key must be a non-empty string.'; 7 | 8 | const result = keyValidator(null, 'key'); 9 | 10 | expect(result).toHaveProperty('valid', false); 11 | expect(result).toHaveProperty('error', expected); 12 | expect(result).not.toHaveProperty('value'); 13 | done(); 14 | }); 15 | 16 | test('should return error on empty', done => { 17 | const expected = 'you passed an empty string, key must be a non-empty string.'; 18 | 19 | const result = keyValidator('', 'key'); 20 | 21 | expect(result).toHaveProperty('valid', false); 22 | expect(result).toHaveProperty('error', expected); 23 | expect(result).not.toHaveProperty('value'); 24 | done(); 25 | }); 26 | 27 | test('should return error on trim', done => { 28 | const expected = 'you passed an empty string, key must be a non-empty string.'; 29 | 30 | const result = keyValidator(' ', 'key'); 31 | 32 | expect(result).toHaveProperty('valid', false); 33 | expect(result).toHaveProperty('error', expected); 34 | expect(result).not.toHaveProperty('value'); 35 | done(); 36 | }); 37 | 38 | test('should return error when key is too long', done => { 39 | let keyInput = getLongKey(); 40 | 41 | const expected = 'key too long, key must be 250 characters or less.'; 42 | 43 | const result = keyValidator(keyInput, 'key'); 44 | 45 | expect(result).toHaveProperty('valid', false); 46 | expect(result).toHaveProperty('error', expected); 47 | expect(result).not.toHaveProperty('value'); 48 | done(); 49 | }); 50 | 51 | test('should return error when key is invalid', done => { 52 | const expected = 'you passed an invalid key, key must be a non-empty string.'; 53 | 54 | const result = keyValidator(true, 'key'); 55 | 56 | expect(result).toHaveProperty('valid', false); 57 | expect(result).toHaveProperty('error', expected); 58 | expect(result).not.toHaveProperty('value'); 59 | done(); 60 | }); 61 | 62 | test('should be valid when is Number', done => { 63 | const result = keyValidator(12345, 'key'); 64 | 65 | expect(result).toHaveProperty('valid', true); 66 | expect(result).toHaveProperty('value', '12345'); 67 | expect(result).not.toHaveProperty('error'); 68 | done(); 69 | }); 70 | 71 | test('should be valid when ok', done => { 72 | const result = keyValidator('key', 'key'); 73 | 74 | expect(result).toHaveProperty('valid', true); 75 | expect(result).toHaveProperty('value', 'key'); 76 | expect(result).not.toHaveProperty('error'); 77 | done(); 78 | }); 79 | 80 | test('should be valid when ok and should trim', done => { 81 | const result = keyValidator(' key ', 'key'); 82 | 83 | expect(result).toHaveProperty('valid', true); 84 | expect(result).toHaveProperty('value', 'key'); 85 | expect(result).not.toHaveProperty('error'); 86 | done(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/keys.test.js: -------------------------------------------------------------------------------- 1 | const keysValidator = require('../keys'); 2 | const { getLongKey } = require('../../testWrapper'); 3 | 4 | describe('keys validator', () => { 5 | test('should return error on null keys', done => { 6 | const expected = 'you passed null or undefined keys, keys must be a non-empty array.'; 7 | 8 | const result = keysValidator(null); 9 | 10 | expect(result).toHaveProperty('valid', false); 11 | expect(result).toHaveProperty('error', expected); 12 | expect(result).not.toHaveProperty('value'); 13 | done(); 14 | }); 15 | 16 | test('should return error on invalid keys', done => { 17 | const expected = 'keys must be a valid format.'; 18 | 19 | const result = keysValidator('test'); 20 | 21 | expect(result).toHaveProperty('valid', false); 22 | expect(result).toHaveProperty('error', expected); 23 | expect(result).not.toHaveProperty('value'); 24 | done(); 25 | }); 26 | 27 | test('should return error on invalid keys 2', done => { 28 | const expected = 'keys must be a valid format.'; 29 | 30 | const result = keysValidator('{}'); 31 | 32 | expect(result).toHaveProperty('valid', false); 33 | expect(result).toHaveProperty('error', expected); 34 | expect(result).not.toHaveProperty('value'); 35 | done(); 36 | }); 37 | 38 | test('should return error when keys is an empty array', done => { 39 | const expected = 'there should be at least one matchingKey-trafficType element.'; 40 | 41 | const result = keysValidator('[]'); 42 | 43 | expect(result).toHaveProperty('valid', false); 44 | expect(result).toHaveProperty('error', expected); 45 | expect(result).not.toHaveProperty('value'); 46 | done(); 47 | }); 48 | 49 | test('should return error when keys is an invalid array', done => { 50 | const expected = 'keys must be a valid format.'; 51 | 52 | const result = keysValidator('["test":true]'); 53 | 54 | expect(result).toHaveProperty('valid', false); 55 | expect(result).toHaveProperty('error', expected); 56 | expect(result).not.toHaveProperty('value'); 57 | done(); 58 | }); 59 | 60 | test('should return error when keys are missing trafficType', done => { 61 | const expected = 'keys is array but there are errors inside of it. keys must be an array with at least one element that contain a valid matchingKey and trafficType. It can also includes bucketingKey.'; 62 | 63 | const result = keysValidator('[{"matchingKey":"my-key"},{"matchingKey":"my-other-key"}]'); 64 | 65 | expect(result).toHaveProperty('valid', false); 66 | expect(result).toHaveProperty('error', expected); 67 | expect(result).not.toHaveProperty('value'); 68 | done(); 69 | }); 70 | 71 | test('should return error when keys are missing matchingKey', done => { 72 | const expected = 'keys is array but there are errors inside of it. keys must be an array with at least one element that contain a valid matchingKey and trafficType. It can also includes bucketingKey.'; 73 | 74 | const result = keysValidator('[{"trafficType":"traffic-key"}]'); 75 | 76 | expect(result).toHaveProperty('valid', false); 77 | expect(result).toHaveProperty('error', expected); 78 | expect(result).not.toHaveProperty('value'); 79 | done(); 80 | }); 81 | 82 | test('should return error when matchingKey is wrong', done => { 83 | const expected = 'keys is array but there are errors inside of it. keys must be an array with at least one element that contain a valid matchingKey and trafficType. It can also includes bucketingKey.'; 84 | 85 | const key = getLongKey(); 86 | const result = keysValidator(`[{"matchingKey":"${key}","trafficType":"my-tt","bucketingKey":"my-bk"}]`); 87 | 88 | expect(result).toHaveProperty('valid', false); 89 | expect(result).toHaveProperty('error', expected); 90 | expect(result).not.toHaveProperty('value'); 91 | done(); 92 | }); 93 | 94 | test('should return error when matchingKey is not string', done => { 95 | const expected = 'keys is array but there are errors inside of it. keys must be an array with at least one element that contain a valid matchingKey and trafficType. It can also includes bucketingKey.'; 96 | 97 | const result = keysValidator('[{"matchingKey": [], "trafficType":"my-tt", "bucketingKey":"my-bk"}]'); 98 | 99 | expect(result).toHaveProperty('valid', false); 100 | expect(result).toHaveProperty('error', expected); 101 | expect(result).not.toHaveProperty('value'); 102 | done(); 103 | }); 104 | 105 | test('should return error when bucketingKey is wrong', done => { 106 | const expected = 'keys is array but there are errors inside of it. keys must be an array with at least one element that contain a valid matchingKey and trafficType. It can also includes bucketingKey.'; 107 | 108 | const result = keysValidator('[{"matchingKey":"my-key", "trafficType":"my-tt", "bucketingKey":" "}]'); 109 | 110 | expect(result).toHaveProperty('valid', false); 111 | expect(result).toHaveProperty('error', expected); 112 | expect(result).not.toHaveProperty('value'); 113 | done(); 114 | }); 115 | 116 | test('should return error when tt is duplicated', done => { 117 | const expected = 'at least one trafficType is duplicated in keys object.'; 118 | 119 | const result = keysValidator('[{"matchingKey":"my-key", "trafficType":"my-tt"},{"matchingKey":"my-key-2", "trafficType":"my-tt-2"},{"matchingKey":"my-key", "trafficType":"my-tt"}]'); 120 | 121 | expect(result).toHaveProperty('valid', false); 122 | expect(result).toHaveProperty('error', expected); 123 | expect(result).not.toHaveProperty('value'); 124 | done(); 125 | }); 126 | 127 | test('should be valid when keys is ok', done => { 128 | const result = keysValidator('[{"matchingKey":"my-key", "trafficType":"my-tt"},{"matchingKey":"my-other-key", "trafficType":"my-tt2"}]'); 129 | 130 | expect(result).toHaveProperty('valid', true); 131 | expect(result).toHaveProperty('value'); 132 | expect(result.value[0].matchingKey).toEqual('my-key'); 133 | expect(result.value[0].trafficType).toEqual('my-tt'); 134 | expect(result.value[1].matchingKey).toEqual('my-other-key'); 135 | expect(result.value[1].trafficType).toEqual('my-tt2'); 136 | expect(result).not.toHaveProperty('error'); 137 | done(); 138 | }); 139 | 140 | test('should be valid when matchingKey is number', done => { 141 | const result = keysValidator('[{"matchingKey":12345, "trafficType":"my-tt"},{"matchingKey":"my-other-key", "trafficType":"my-tt2"}]'); 142 | 143 | expect(result).toHaveProperty('valid', true); 144 | expect(result).toHaveProperty('value'); 145 | expect(result.value[0].matchingKey).toEqual('12345'); 146 | expect(result.value[0].trafficType).toEqual('my-tt'); 147 | expect(result.value[1].matchingKey).toEqual('my-other-key'); 148 | expect(result.value[1].trafficType).toEqual('my-tt2'); 149 | expect(result).not.toHaveProperty('error'); 150 | done(); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/ok.test.js: -------------------------------------------------------------------------------- 1 | const okWrapper = require('../wrapper/ok'); 2 | 3 | test('should parse different values', done => { 4 | let result = okWrapper('string'); 5 | expect(result).toHaveProperty('valid', true); 6 | expect(result).toHaveProperty('value', 'string'); 7 | expect(result).not.toHaveProperty('error'); 8 | 9 | result = okWrapper(null); 10 | expect(result).toHaveProperty('valid', true); 11 | expect(result).toHaveProperty('value', null); 12 | expect(result).not.toHaveProperty('error'); 13 | 14 | result = okWrapper(12345); 15 | expect(result).toHaveProperty('valid', true); 16 | expect(result).toHaveProperty('value', 12345); 17 | expect(result).not.toHaveProperty('error'); 18 | 19 | result = okWrapper({ test: '1' }); 20 | expect(result).toHaveProperty('valid', true); 21 | expect(result).toHaveProperty('value', { test: '1' }); 22 | expect(result).not.toHaveProperty('error'); 23 | 24 | done(); 25 | }); -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/properties.test.js: -------------------------------------------------------------------------------- 1 | const propertiesValidator = require('../properties'); 2 | 3 | describe('properties validator', () => { 4 | test('should return error on invalid properties', done => { 5 | const expected = 'properties must be a plain object.'; 6 | 7 | const result = propertiesValidator('test'); 8 | 9 | expect(result).toHaveProperty('valid', false); 10 | expect(result).toHaveProperty('error', expected); 11 | expect(result).not.toHaveProperty('value'); 12 | done(); 13 | }); 14 | 15 | test('should return error on invalid properties 2', done => { 16 | const expected = 'properties must be a plain object.'; 17 | 18 | const result = propertiesValidator('[]'); 19 | 20 | expect(result).toHaveProperty('valid', false); 21 | expect(result).toHaveProperty('error', expected); 22 | expect(result).not.toHaveProperty('value'); 23 | done(); 24 | }); 25 | 26 | test('should return error on invalid properties 3', done => { 27 | const expected = 'properties must be a plain object.'; 28 | 29 | const result = propertiesValidator('true'); 30 | 31 | expect(result).toHaveProperty('valid', false); 32 | expect(result).toHaveProperty('error', expected); 33 | expect(result).not.toHaveProperty('value'); 34 | done(); 35 | }); 36 | 37 | test('should be valid when properties is an object', done => { 38 | const result = propertiesValidator('{"my-prop":true}'); 39 | expect(result).toHaveProperty('valid', true); 40 | expect(result).toHaveProperty('value', { 41 | 'my-prop': true, 42 | }); 43 | expect(result).not.toHaveProperty('error'); 44 | done(); 45 | }); 46 | 47 | test('should be valid when properties is empty object', done => { 48 | const result = propertiesValidator('{}'); 49 | 50 | expect(result).toHaveProperty('valid', true); 51 | expect(result).toHaveProperty('value', {}); 52 | expect(result).not.toHaveProperty('error'); 53 | done(); 54 | }); 55 | 56 | test('should be valid when properties is null', done => { 57 | const result = propertiesValidator(); 58 | 59 | expect(result).toHaveProperty('valid', true); 60 | expect(result).toHaveProperty('value', null); 61 | expect(result).not.toHaveProperty('error'); 62 | done(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/split.test.js: -------------------------------------------------------------------------------- 1 | const splitValidator = require('../split'); 2 | 3 | describe('split validator', () => { 4 | test('should return error on undefined', done => { 5 | const expected = 'you passed a null or undefined split-name, split-name must be a non-empty string.'; 6 | 7 | const result = splitValidator(); 8 | 9 | expect(result).toHaveProperty('valid', false); 10 | expect(result).toHaveProperty('error', expected); 11 | expect(result).not.toHaveProperty('value'); 12 | done(); 13 | }); 14 | 15 | test('should return error on empty', done => { 16 | const expected = 'you passed an empty split-name, split-name must be a non-empty string.'; 17 | 18 | const result = splitValidator(''); 19 | 20 | expect(result).toHaveProperty('valid', false); 21 | expect(result).toHaveProperty('error', expected); 22 | expect(result).not.toHaveProperty('value'); 23 | done(); 24 | }); 25 | 26 | test('should return error on trim', done => { 27 | const expected = 'you passed an empty split-name, split-name must be a non-empty string.'; 28 | 29 | const result = splitValidator(' '); 30 | 31 | expect(result).toHaveProperty('valid', false); 32 | expect(result).toHaveProperty('error', expected); 33 | expect(result).not.toHaveProperty('value'); 34 | done(); 35 | }); 36 | 37 | test('should be valid when ok', done => { 38 | const result = splitValidator('my-split'); 39 | 40 | expect(result).toHaveProperty('valid', true); 41 | expect(result).toHaveProperty('value', 'my-split'); 42 | expect(result).not.toHaveProperty('error'); 43 | done(); 44 | }); 45 | 46 | test('should be valid when ok and should trim', done => { 47 | const result = splitValidator(' my-split '); 48 | 49 | expect(result).toHaveProperty('valid', true); 50 | expect(result).toHaveProperty('value', 'my-split'); 51 | expect(result).not.toHaveProperty('error'); 52 | done(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/splits.test.js: -------------------------------------------------------------------------------- 1 | const splitsValidator = require('../splits'); 2 | 3 | describe('feature flags validator', () => { 4 | test('should return error on undefined', done => { 5 | const expected = 'you passed a null or undefined split-names, split-names must be a non-empty array.'; 6 | 7 | const result = splitsValidator(); 8 | 9 | expect(result).toHaveProperty('valid', false); 10 | expect(result).toHaveProperty('error', expected); 11 | expect(result).not.toHaveProperty('value'); 12 | done(); 13 | }); 14 | 15 | test('should return error on empty', done => { 16 | const expected = 'split-names must be a non-empty array.'; 17 | 18 | const result = splitsValidator(''); 19 | 20 | expect(result).toHaveProperty('valid', false); 21 | expect(result).toHaveProperty('error', expected); 22 | expect(result).not.toHaveProperty('value'); 23 | done(); 24 | }); 25 | 26 | test('should return error on trim', done => { 27 | const expected = 'split-names must be a non-empty array.'; 28 | 29 | const result = splitsValidator(' '); 30 | 31 | expect(result).toHaveProperty('valid', false); 32 | expect(result).toHaveProperty('error', expected); 33 | expect(result).not.toHaveProperty('value'); 34 | done(); 35 | }); 36 | 37 | test('should be valid when ok', done => { 38 | const result = splitsValidator('my-split'); 39 | 40 | expect(result).toHaveProperty('valid', true); 41 | expect(result).toHaveProperty('value', ['my-split']); 42 | expect(result).not.toHaveProperty('error'); 43 | done(); 44 | }); 45 | 46 | test('should be valid when ok and should trim', done => { 47 | const result = splitsValidator(' my-split '); 48 | 49 | expect(result).toHaveProperty('valid', true); 50 | expect(result).toHaveProperty('value', ['my-split']); 51 | expect(result).not.toHaveProperty('error'); 52 | done(); 53 | }); 54 | 55 | test('should be valid on multiple inputs and repeated splits', done => { 56 | const result = splitsValidator(' my-split ,my-split2, my-split, test'); 57 | 58 | expect(result).toHaveProperty('valid', true); 59 | expect(result).toHaveProperty('value', ['my-split','my-split2','test']); 60 | expect(result).not.toHaveProperty('error'); 61 | done(); 62 | }); 63 | }); -------------------------------------------------------------------------------- /utils/inputValidation/__tests__/trafficType.test.js: -------------------------------------------------------------------------------- 1 | const trafficTypeValidator = require('../trafficType'); 2 | 3 | describe('trafficType validator', () => { 4 | test('should return error on undefined', done => { 5 | const expected = 'you passed a null or undefined traffic-type, traffic-type must be a non-empty string.'; 6 | 7 | const result = trafficTypeValidator(); 8 | 9 | expect(result).toHaveProperty('valid', false); 10 | expect(result).toHaveProperty('error', expected); 11 | expect(result).not.toHaveProperty('value'); 12 | done(); 13 | }); 14 | 15 | test('should return error on empty', done => { 16 | const expected = 'you passed an empty traffic-type, traffic-type must be a non-empty string.'; 17 | 18 | const result = trafficTypeValidator(''); 19 | 20 | expect(result).toHaveProperty('valid', false); 21 | expect(result).toHaveProperty('error', expected); 22 | expect(result).not.toHaveProperty('value'); 23 | done(); 24 | }); 25 | 26 | test('should be valid when ok', done => { 27 | const result = trafficTypeValidator('my-traffic-type'); 28 | 29 | expect(result).toHaveProperty('valid', true); 30 | expect(result).toHaveProperty('value', 'my-traffic-type'); 31 | expect(result).not.toHaveProperty('error'); 32 | done(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /utils/inputValidation/attributes.js: -------------------------------------------------------------------------------- 1 | const { isObject, isString } = require('../lang'); 2 | const errorWrapper = require('./wrapper/error'); 3 | const okWrapper = require('./wrapper/ok'); 4 | 5 | const validateAttributes = (maybeAttributes) => { 6 | // eslint-disable-next-line eqeqeq 7 | if (maybeAttributes == undefined) { 8 | return okWrapper(null); 9 | } 10 | try { 11 | let attributes; 12 | if (isString(maybeAttributes)) { // If it came as a query param, parse it from string 13 | maybeAttributes = JSON.parse(maybeAttributes); 14 | } 15 | if (isObject(maybeAttributes)) { // Parsed query or coming from a JSON payload, check if it's an object. 16 | attributes = maybeAttributes; 17 | } 18 | return (isObject(attributes)) ? okWrapper(attributes) : errorWrapper('attributes must be a plain object.'); 19 | } catch (e) { 20 | return errorWrapper('attributes must be a plain object.'); 21 | } 22 | }; 23 | 24 | module.exports = validateAttributes; -------------------------------------------------------------------------------- /utils/inputValidation/eventType.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const EVENT_TYPE_REGEX = /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/; 4 | 5 | const validateEventType = (maybeEventType) => { 6 | // eslint-disable-next-line eqeqeq 7 | if (maybeEventType == undefined) return errorWrapper('you passed a null or undefined event-type, event-type must be a non-empty string.'); 8 | if (maybeEventType.length === 0) return errorWrapper('you passed an empty event-type, event-type must be a non-empty string.'); 9 | if (!EVENT_TYPE_REGEX.test(maybeEventType)) return errorWrapper(`you passed "${maybeEventType}", event-type must adhere to the regular expression /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/g. This means an event_type must be alphanumeric, cannot be more than 80 characters long, and can only include a dash, underscore, period, or colon as separators of alphanumeric characters.`); 10 | 11 | return okWrapper(maybeEventType); 12 | }; 13 | 14 | module.exports = validateEventType; -------------------------------------------------------------------------------- /utils/inputValidation/flagSet.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const { NULL_FLAG_SET, EMPTY_FLAG_SET, TRIMMABLE_SPACES_REGEX } = require('../constants'); 4 | 5 | const validateFlagSet = (maybeFlagSet) => { 6 | // eslint-disable-next-line 7 | if (maybeFlagSet == undefined) return errorWrapper(NULL_FLAG_SET); 8 | 9 | if (TRIMMABLE_SPACES_REGEX.test(maybeFlagSet)) { 10 | console.log(`flag-sets "${maybeFlagSet}" has extra whitespace, trimming.`); 11 | maybeFlagSet = maybeFlagSet.trim(); 12 | } 13 | if (maybeFlagSet.length === 0) return errorWrapper(EMPTY_FLAG_SET); 14 | 15 | return okWrapper(maybeFlagSet); 16 | }; 17 | 18 | module.exports = validateFlagSet; -------------------------------------------------------------------------------- /utils/inputValidation/flagSets.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const lang = require('../lang'); 4 | const validateFlagSet = require('./flagSet'); 5 | const { EMPTY_FLAG_SETS, NULL_FLAG_SETS } = require('../constants'); 6 | 7 | const validateFlagSets = (maybeFlagSets) => { 8 | // eslint-disable-next-line eqeqeq 9 | if (maybeFlagSets == undefined) return errorWrapper(NULL_FLAG_SETS); 10 | 11 | maybeFlagSets = maybeFlagSets.split(','); 12 | 13 | if (maybeFlagSets.length > 0) { 14 | let validatedArray = []; 15 | // Remove invalid values 16 | maybeFlagSets.forEach(maybeFlagSet => { 17 | const flagSetValidation = validateFlagSet(maybeFlagSet); 18 | if (flagSetValidation.valid) validatedArray.push(flagSetValidation.value); 19 | }); 20 | 21 | // Strip off duplicated values if we have valid flag sets then return 22 | if (validatedArray.length) return okWrapper(lang.uniq(validatedArray)); 23 | } 24 | 25 | return errorWrapper(EMPTY_FLAG_SETS); 26 | }; 27 | 28 | module.exports = validateFlagSets; -------------------------------------------------------------------------------- /utils/inputValidation/key.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const { isString, isFinite } = require('../lang'); 4 | 5 | const KEY_MAX_LENGTH = 250; 6 | 7 | const validateKeyValue = (maybeKey, type) => { 8 | // eslint-disable-next-line eqeqeq 9 | if (maybeKey == undefined) return errorWrapper(`you passed a null or undefined ${type}, ${type} must be a non-empty string.`); 10 | 11 | if (isString(maybeKey)) { 12 | // It's a string, start by trimming the value. 13 | maybeKey = maybeKey.trim(); 14 | } else if (isFinite(maybeKey)) { 15 | maybeKey = maybeKey.toString(); 16 | } else return errorWrapper('you passed an invalid key, key must be a non-empty string.'); 17 | 18 | // It's aaaaaall good. 19 | if (maybeKey.length > 0 && maybeKey.length <= KEY_MAX_LENGTH)return okWrapper(maybeKey); 20 | 21 | if (maybeKey.length === 0) return errorWrapper(`you passed an empty string, ${type} must be a non-empty string.`); 22 | 23 | return errorWrapper(`${type} too long, ${type} must be 250 characters or less.`); 24 | }; 25 | 26 | module.exports = validateKeyValue; -------------------------------------------------------------------------------- /utils/inputValidation/keys.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const trafficTypeValidator = require('./trafficType'); 4 | const keyValidator = require('./key'); 5 | 6 | const validateKeys = (maybeKeys) => { 7 | // eslint-disable-next-line eqeqeq 8 | if (maybeKeys == undefined) return errorWrapper('you passed null or undefined keys, keys must be a non-empty array.'); 9 | try { 10 | const keys = JSON.parse(maybeKeys); 11 | if (!Array.isArray(keys)) return errorWrapper('keys must be a valid format.'); 12 | if (keys.length === 0) return errorWrapper('there should be at least one matchingKey-trafficType element.'); 13 | 14 | const validKeys = []; 15 | 16 | const trafficTypes = new Set; 17 | let trafficTypeDuplicated = false; 18 | 19 | const isInvalid = keys.some(key => { 20 | const trafficTypeValidation = trafficTypeValidator(key.trafficType); 21 | const matchingKeyValidation = keyValidator(key.matchingKey, 'matchingKey'); 22 | const bucketingKeyValidation = key.bucketingKey !== undefined ? keyValidator(key.bucketingKey, 'bucketingKey') : okWrapper(null); 23 | 24 | if (!trafficTypeValidation.valid || !matchingKeyValidation.valid || !bucketingKeyValidation.valid) return true; 25 | 26 | if (trafficTypes.has(trafficTypeValidation.value)) { 27 | trafficTypeDuplicated = true; 28 | } 29 | trafficTypes.add(trafficTypeValidation.value); 30 | validKeys.push({ 31 | trafficType: trafficTypeValidation.value, 32 | matchingKey: matchingKeyValidation.value, 33 | bucketingKey: bucketingKeyValidation.value, 34 | }); 35 | }); 36 | 37 | if (isInvalid) return errorWrapper('keys is array but there are errors inside of it. keys must be an array with at least one element that contain a valid matchingKey and trafficType. It can also includes bucketingKey.'); 38 | 39 | return trafficTypeDuplicated ? errorWrapper('at least one trafficType is duplicated in keys object.') : okWrapper(validKeys); 40 | } catch (e) { 41 | return errorWrapper('keys must be a valid format.'); 42 | } 43 | }; 44 | 45 | module.exports = validateKeys; -------------------------------------------------------------------------------- /utils/inputValidation/properties.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const lang = require('../lang'); 4 | 5 | const validateProperties = (maybeProperties) => { 6 | // eslint-disable-next-line eqeqeq 7 | if (maybeProperties == undefined) return okWrapper(null); 8 | 9 | try { 10 | const properties = JSON.parse(maybeProperties); 11 | return (lang.isObject(properties)) ? okWrapper(properties) : errorWrapper('properties must be a plain object.'); 12 | } catch (e) { 13 | return errorWrapper('properties must be a plain object.'); 14 | } 15 | }; 16 | 17 | module.exports = validateProperties; -------------------------------------------------------------------------------- /utils/inputValidation/split.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const { TRIMMABLE_SPACES_REGEX } = require('../constants'); 4 | 5 | const validateSplit = (maybeSplit) => { 6 | // eslint-disable-next-line eqeqeq 7 | if (maybeSplit == undefined) return errorWrapper('you passed a null or undefined split-name, split-name must be a non-empty string.'); 8 | 9 | if (TRIMMABLE_SPACES_REGEX.test(maybeSplit)) { 10 | console.log(`split-name "${maybeSplit}" has extra whitespace, trimming.`); 11 | maybeSplit = maybeSplit.trim(); 12 | } 13 | 14 | return maybeSplit.length > 0 ? okWrapper(maybeSplit) : errorWrapper('you passed an empty split-name, split-name must be a non-empty string.'); 15 | }; 16 | 17 | module.exports = validateSplit; -------------------------------------------------------------------------------- /utils/inputValidation/splits.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const lang = require('../lang'); 4 | const splitValidator = require('./split'); 5 | 6 | const validateSplits = (maybeFeatureFlags) => { 7 | // eslint-disable-next-line eqeqeq 8 | if (maybeFeatureFlags == undefined) return errorWrapper('you passed a null or undefined split-names, split-names must be a non-empty array.'); 9 | 10 | maybeFeatureFlags = maybeFeatureFlags.split(','); 11 | 12 | if (maybeFeatureFlags.length > 0) { 13 | let validatedArray = []; 14 | // Remove invalid values 15 | maybeFeatureFlags.forEach(maybeSplit => { 16 | const splitValidation = splitValidator(maybeSplit); 17 | if (splitValidation.valid) validatedArray.push(splitValidation.value); 18 | }); 19 | 20 | // Strip off duplicated values if we have valid split names then return 21 | if (validatedArray.length) return okWrapper(lang.uniq(validatedArray)); 22 | } 23 | 24 | return errorWrapper('split-names must be a non-empty array.'); 25 | }; 26 | 27 | module.exports = validateSplits; -------------------------------------------------------------------------------- /utils/inputValidation/trafficType.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | const { isString } = require('../lang'); 4 | 5 | const CAPITAL_LETTERS_REGEX = /[A-Z]/; 6 | 7 | const validateTrafficType = (maybeTT) => { 8 | // eslint-disable-next-line eqeqeq 9 | if (maybeTT == undefined) return errorWrapper('you passed a null or undefined traffic-type, traffic-type must be a non-empty string.'); 10 | 11 | if (isString(maybeTT)) { 12 | maybeTT = maybeTT.trim(); 13 | if (maybeTT.length === 0) return errorWrapper('you passed an empty traffic-type, traffic-type must be a non-empty string.'); 14 | 15 | if (CAPITAL_LETTERS_REGEX.test(maybeTT)) { 16 | maybeTT = maybeTT.toLowerCase(); 17 | } 18 | 19 | return okWrapper(maybeTT); 20 | } 21 | 22 | return errorWrapper('you passed an invalid traffic-type, traffic-type must be a non-empty string.'); 23 | }; 24 | 25 | module.exports = validateTrafficType; -------------------------------------------------------------------------------- /utils/inputValidation/value.js: -------------------------------------------------------------------------------- 1 | const errorWrapper = require('./wrapper/error'); 2 | const okWrapper = require('./wrapper/ok'); 3 | 4 | const validateValue = (maybeValue) => { 5 | // eslint-disable-next-line eqeqeq 6 | if (maybeValue == undefined) return okWrapper(null); 7 | 8 | if (maybeValue.length === 0 || maybeValue.trim().length === 0) return errorWrapper('value must be null or number.'); 9 | if (isNaN(Number(maybeValue))) return errorWrapper('value must be null or number.'); 10 | 11 | return okWrapper(Number(maybeValue)); 12 | }; 13 | 14 | module.exports = validateValue; -------------------------------------------------------------------------------- /utils/inputValidation/wrapper/error.js: -------------------------------------------------------------------------------- 1 | const buildErrorMessage = error => ({ 2 | error, 3 | valid: false, 4 | }); 5 | 6 | module.exports = buildErrorMessage; -------------------------------------------------------------------------------- /utils/inputValidation/wrapper/ok.js: -------------------------------------------------------------------------------- 1 | const buildOK = value => ({ 2 | value, 3 | valid: true, 4 | }); 5 | 6 | module.exports = buildOK; -------------------------------------------------------------------------------- /utils/lang/__tests__/isFInite.test.js: -------------------------------------------------------------------------------- 1 | const { isFinite } = require('..'); 2 | 3 | test('isFinite', done => { 4 | expect(isFinite('test')).toBe(false); 5 | expect(isFinite(true)).toBe(false); 6 | expect(isFinite([])).toBe(false); 7 | expect(isFinite(() => true)).toBe(false); 8 | expect(isFinite(JSON.parse('{"test": 1}'))).toBe(false); 9 | expect(isFinite(12345)).toBe(true); 10 | done(); 11 | }); -------------------------------------------------------------------------------- /utils/lang/__tests__/isObject.test.js: -------------------------------------------------------------------------------- 1 | const { isObject } = require('../'); 2 | 3 | test('isObject', done => { 4 | expect(isObject('test')).toBe(false); 5 | expect(isObject(true)).toBe(false); 6 | expect(isObject([])).toBe(false); 7 | expect(isObject(12345)).toBe(false); 8 | expect(isObject(() => true)).toBe(false); 9 | expect(isObject(JSON.parse('{"test": 1}'))).toBe(true); 10 | expect(isObject({})).toBe(true); 11 | done(); 12 | }); -------------------------------------------------------------------------------- /utils/lang/__tests__/isString.test.js: -------------------------------------------------------------------------------- 1 | const { isString } = require('../'); 2 | 3 | test('isString', done => { 4 | expect(isString(true)).toBe(false); 5 | expect(isString([])).toBe(false); 6 | expect(isString(() => true)).toBe(false); 7 | expect(isString(JSON.parse('{"test": 1}'))).toBe(false); 8 | expect(isString(12345)).toBe(false); 9 | expect(isString('test')).toBe(true); 10 | done(); 11 | }); -------------------------------------------------------------------------------- /utils/lang/__tests__/uniq.test.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('../'); 2 | 3 | test('uniq', done => { 4 | expect(uniq([])).toEqual([]); 5 | expect(uniq(['test'])).toEqual(['test']); 6 | expect(uniq(['test', 'test'])).toEqual(['test']); 7 | expect(uniq(['test', 'test2', 'test'])).toEqual(['test', 'test2']); 8 | done(); 9 | }); -------------------------------------------------------------------------------- /utils/lang/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if a given value is a finite number. 3 | */ 4 | const isFinite = (val) => { 5 | if (typeof val === 'number') return Number.isFinite(val); 6 | if (val instanceof Number) return Number.isFinite(val.valueOf()); 7 | 8 | return false; 9 | }; 10 | 11 | /** 12 | * Checks if a given value is a string. 13 | */ 14 | const isString = (val) => typeof val === 'string' || val instanceof String; 15 | 16 | /** 17 | * Validates if a value is an object. 18 | */ 19 | const isObject = (obj) => obj && typeof obj === 'object' && obj.constructor === Object; 20 | 21 | /** 22 | * Removes duplicate items on an array of strings. 23 | */ 24 | const uniq = (arr) => arr.filter((v, i, a) => a.indexOf(v) === i); 25 | 26 | module.exports = { 27 | isFinite, 28 | isObject, 29 | isString, 30 | uniq, 31 | }; 32 | -------------------------------------------------------------------------------- /utils/mocks/index.js: -------------------------------------------------------------------------------- 1 | const apiKeyMocksMap = { 2 | 'localhost': { 3 | splitUrl: '/parserConfigs/split.yml', 4 | splitNames: ['my-experiment','other-experiment-3','other-experiment','other-experiment-2'], 5 | segments: [], 6 | lastSynchronization: { 7 | sp: 1674855033145, 8 | to: 1674855033805, 9 | te: 1674855034008, 10 | im: 1674855034868, 11 | }, 12 | timeUntilReady: 605, 13 | httpErrors: {}, 14 | }, 15 | 'apikey1': { 16 | splitUrl: '/split1.yml', 17 | splitNames: ['testing_split_blue','testing_split_color','testing_split_only_wl','testing_split_with_wl','testing_split_with_config'], 18 | segments: [], 19 | lastSynchronization: { 20 | sp: 1674857486785, 21 | to: 1674857487438, 22 | te: 1674857487594, 23 | }, 24 | timeUntilReady: 1000, 25 | httpErrors: {}, 26 | }, 27 | 'apikey2': { 28 | splitUrl: '/split2.yml', 29 | splitNames: ['testing_split_red','testing_split_color','testing_split_only_wl','testing_split_with_wl','testing_split_with_config'], 30 | segments: [], 31 | lastSynchronization: { 32 | sp: 1674857489875, 33 | to: 1674857489888, 34 | te: 1674857488686, 35 | }, 36 | timeUntilReady: 860, 37 | httpErrors: {}, 38 | }, 39 | }; 40 | 41 | const core = { 42 | authorizationKey: 'API_KEY', 43 | labelIsEnabled: false, 44 | IPAddressesEnabled: false, 45 | }; 46 | 47 | const scheduler = { 48 | featuresRefreshRate: 7, 49 | segmentsRefreshRate: 7, 50 | impressionsRefreshRate: 7, 51 | impressionsQueueSize: 7, 52 | eventsPushRate: 7, 53 | eventsQueueSize: 7, 54 | metricsRefreshRate: 7, 55 | }; 56 | 57 | const urls = { 58 | sdk: 'https://sdk.global-split.io/api', 59 | events: 'https://events.global-split.io/api', 60 | auth: 'https://auth.global-split.io/api', 61 | streaming: 'https://streaming.global-split.io', 62 | telemetry: 'https://telemetry.global-split.io/api', 63 | }; 64 | 65 | const startup = { 66 | requestTimeoutBeforeReady: 7, 67 | retriesOnFailureBeforeReady: 7, 68 | readyTimeout: 7, 69 | }; 70 | 71 | const storage = { 72 | type: 'redis', 73 | prefix: 'SPLITIO', 74 | }; 75 | 76 | const sync = { 77 | impressionsMode: 'NONE', 78 | enabled: false, 79 | }; 80 | 81 | const integrations = [{ 82 | type: 'GOOGLE_ANALYTICS_TO_SPLIT', 83 | }]; 84 | 85 | const expectedGreenResults = { 86 | 'test_green': { 87 | treatment: 'on', 88 | }, 89 | 'test_color': { 90 | treatment: 'on', 91 | }, 92 | 'test_green_config': { 93 | treatment: 'on', 94 | }, 95 | }; 96 | const expectedPurpleResults = { 97 | 'test_purple': { 98 | treatment: 'on', 99 | }, 100 | 'test_color': { 101 | treatment: 'on', 102 | }, 103 | 'test_purple_config': { 104 | treatment: 'on', 105 | }, 106 | }; 107 | const expectedPinkResults = { 108 | ...expectedGreenResults, 109 | ...expectedPurpleResults, 110 | }; 111 | 112 | const expectedGreenResultsWithConfig = { 113 | 'test_green': { 114 | treatment: 'on', 115 | }, 116 | 'test_color': { 117 | treatment: 'on', 118 | }, 119 | 'test_green_config': { 120 | treatment: 'on', 121 | config: '{"color":"green"}', 122 | }, 123 | }; 124 | 125 | const expectedPurpleResultsWithConfig = { 126 | 'test_purple': { 127 | treatment: 'on', 128 | }, 129 | 'test_color': { 130 | treatment: 'on', 131 | }, 132 | 'test_purple_config': { 133 | treatment: 'on', 134 | config: '{"color":"purple"}', 135 | }, 136 | }; 137 | 138 | const expectedPinkResultsWithConfig = { 139 | ...expectedGreenResultsWithConfig, 140 | ...expectedPurpleResultsWithConfig, 141 | }; 142 | 143 | module.exports = { 144 | core, scheduler, urls, startup, storage, sync, integrations, apiKeyMocksMap, 145 | expectedGreenResults, 146 | expectedPurpleResults, 147 | expectedPinkResults, 148 | expectedGreenResultsWithConfig, 149 | expectedPurpleResultsWithConfig, 150 | expectedPinkResultsWithConfig, 151 | }; -------------------------------------------------------------------------------- /utils/mocks/splitchanges.since.-1.till.1602796638344.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "trafficTypeName": "client", 5 | "name": "test_green", 6 | "trafficAllocation": 100, 7 | "trafficAllocationSeed": 147392224, 8 | "seed": 524417105, 9 | "status": "ACTIVE", 10 | "killed": false, 11 | "defaultTreatment": "on", 12 | "changeNumber": 1602796638344, 13 | "algo": 2, 14 | "configurations": {}, 15 | "sets": ["set_green"], 16 | "conditions": [ 17 | { 18 | "conditionType": "ROLLOUT", 19 | "matcherGroup": { 20 | "combiner": "AND", 21 | "matchers": [ 22 | { 23 | "keySelector": { "trafficType": "client", "attribute": null }, 24 | "matcherType": "ALL_KEYS", 25 | "negate": false, 26 | "userDefinedSegmentMatcherData": null, 27 | "whitelistMatcherData": null, 28 | "unaryNumericMatcherData": null, 29 | "betweenMatcherData": null, 30 | "booleanMatcherData": null, 31 | "dependencyMatcherData": null, 32 | "stringMatcherData": null 33 | } 34 | ] 35 | }, 36 | "partitions": [ 37 | { "treatment": "on", "size": 100 }, 38 | { "treatment": "off", "size": 0 }, 39 | { "treatment": "free", "size": 0 }, 40 | { "treatment": "conta", "size": 0 } 41 | ], 42 | "label": "default rule" 43 | } 44 | ] 45 | }, 46 | { 47 | "trafficTypeName": "client", 48 | "name": "test_color", 49 | "trafficAllocation": 100, 50 | "trafficAllocationSeed": 147392224, 51 | "seed": 524417105, 52 | "status": "ACTIVE", 53 | "killed": false, 54 | "defaultTreatment": "on", 55 | "changeNumber": 1602796638344, 56 | "algo": 2, 57 | "configurations": {}, 58 | "sets": ["set_green", "set_purple"], 59 | "conditions": [ 60 | { 61 | "conditionType": "ROLLOUT", 62 | "matcherGroup": { 63 | "combiner": "AND", 64 | "matchers": [ 65 | { 66 | "keySelector": { "trafficType": "client", "attribute": null }, 67 | "matcherType": "ALL_KEYS", 68 | "negate": false, 69 | "userDefinedSegmentMatcherData": null, 70 | "whitelistMatcherData": null, 71 | "unaryNumericMatcherData": null, 72 | "betweenMatcherData": null, 73 | "booleanMatcherData": null, 74 | "dependencyMatcherData": null, 75 | "stringMatcherData": null 76 | } 77 | ] 78 | }, 79 | "partitions": [ 80 | { "treatment": "on", "size": 100 }, 81 | { "treatment": "off", "size": 0 }, 82 | { "treatment": "free", "size": 0 }, 83 | { "treatment": "conta", "size": 0 } 84 | ], 85 | "label": "default rule" 86 | } 87 | ] 88 | }, 89 | { 90 | "trafficTypeName": "client", 91 | "name": "test_green_config", 92 | "trafficAllocation": 100, 93 | "trafficAllocationSeed": 147392224, 94 | "seed": 524417105, 95 | "status": "ACTIVE", 96 | "killed": false, 97 | "defaultTreatment": "on", 98 | "changeNumber": 1602796638344, 99 | "algo": 2, 100 | "configurations": { 101 | "on": "{\"color\":\"green\"}" 102 | }, 103 | "sets": ["set_green"], 104 | "conditions": [ 105 | { 106 | "conditionType": "ROLLOUT", 107 | "matcherGroup": { 108 | "combiner": "AND", 109 | "matchers": [ 110 | { 111 | "keySelector": { "trafficType": "client", "attribute": null }, 112 | "matcherType": "ALL_KEYS", 113 | "negate": false, 114 | "userDefinedSegmentMatcherData": null, 115 | "whitelistMatcherData": null, 116 | "unaryNumericMatcherData": null, 117 | "betweenMatcherData": null, 118 | "booleanMatcherData": null, 119 | "dependencyMatcherData": null, 120 | "stringMatcherData": null 121 | } 122 | ] 123 | }, 124 | "partitions": [ 125 | { "treatment": "on", "size": 100 }, 126 | { "treatment": "off", "size": 0 }, 127 | { "treatment": "free", "size": 0 }, 128 | { "treatment": "conta", "size": 0 } 129 | ], 130 | "label": "default rule" 131 | } 132 | ] 133 | }, 134 | { 135 | "trafficTypeName": "client", 136 | "name": "test_purple", 137 | "trafficAllocation": 100, 138 | "trafficAllocationSeed": 147392224, 139 | "seed": 524417105, 140 | "status": "ACTIVE", 141 | "killed": false, 142 | "defaultTreatment": "on", 143 | "changeNumber": 1602796638344, 144 | "algo": 2, 145 | "configurations": {}, 146 | "sets": ["set_purple"], 147 | "conditions": [ 148 | { 149 | "conditionType": "ROLLOUT", 150 | "matcherGroup": { 151 | "combiner": "AND", 152 | "matchers": [ 153 | { 154 | "keySelector": { "trafficType": "client", "attribute": null }, 155 | "matcherType": "ALL_KEYS", 156 | "negate": false, 157 | "userDefinedSegmentMatcherData": null, 158 | "whitelistMatcherData": null, 159 | "unaryNumericMatcherData": null, 160 | "betweenMatcherData": null, 161 | "booleanMatcherData": null, 162 | "dependencyMatcherData": null, 163 | "stringMatcherData": null 164 | } 165 | ] 166 | }, 167 | "partitions": [ 168 | { "treatment": "on", "size": 100 }, 169 | { "treatment": "off", "size": 0 }, 170 | { "treatment": "free", "size": 0 }, 171 | { "treatment": "conta", "size": 0 } 172 | ], 173 | "label": "default rule" 174 | } 175 | ] 176 | }, 177 | { 178 | "trafficTypeName": "client", 179 | "name": "test_purple_config", 180 | "trafficAllocation": 100, 181 | "trafficAllocationSeed": 147392224, 182 | "seed": 524417105, 183 | "status": "ACTIVE", 184 | "killed": false, 185 | "defaultTreatment": "on", 186 | "changeNumber": 1602796638344, 187 | "algo": 2, 188 | "configurations": { 189 | "on": "{\"color\":\"purple\"}" 190 | }, 191 | "sets": ["set_purple"], 192 | "conditions": [ 193 | { 194 | "conditionType": "ROLLOUT", 195 | "matcherGroup": { 196 | "combiner": "AND", 197 | "matchers": [ 198 | { 199 | "keySelector": { "trafficType": "client", "attribute": null }, 200 | "matcherType": "ALL_KEYS", 201 | "negate": false, 202 | "userDefinedSegmentMatcherData": null, 203 | "whitelistMatcherData": null, 204 | "unaryNumericMatcherData": null, 205 | "betweenMatcherData": null, 206 | "booleanMatcherData": null, 207 | "dependencyMatcherData": null, 208 | "stringMatcherData": null 209 | } 210 | ] 211 | }, 212 | "partitions": [ 213 | { "treatment": "on", "size": 100 }, 214 | { "treatment": "off", "size": 0 }, 215 | { "treatment": "free", "size": 0 }, 216 | { "treatment": "conta", "size": 0 } 217 | ], 218 | "label": "default rule" 219 | } 220 | ] 221 | } 222 | ], 223 | "since": -1, 224 | "till": 1602796638344 225 | } 226 | -------------------------------------------------------------------------------- /utils/parserConfigs/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const settings = require('../index'); 2 | const { core, scheduler, storage, urls, startup, sync, integrations } = require('../../mocks'); 3 | 4 | 5 | describe('getConfigs', () => { 6 | test('apikey', done => { 7 | // Test null 8 | expect(() => settings().toThrow()); 9 | 10 | // Test empty 11 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"","AUTH_TOKEN":"test"}]'; 12 | expect(() => settings().toThrow()); 13 | 14 | // Test trim 15 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":" ","AUTH_TOKEN":"test"}]'; 16 | expect(() => settings().toThrow()); 17 | 18 | // Test ok 19 | process.env.SPLIT_EVALUATOR_ENVIRONMENTS='[{"API_KEY":"something","AUTH_TOKEN":"test"}]'; 20 | expect(() => settings().not.toThrow()); 21 | 22 | const options = settings(); 23 | expect(options).not.toHaveProperty('impressionListener'); 24 | expect(options).not.toHaveProperty('scheduler'); 25 | expect(options).not.toHaveProperty('urls'); 26 | done(); 27 | }); 28 | 29 | test('log level', done => { 30 | // Test empty 31 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = ''; 32 | expect(() => settings().toThrow()); 33 | 34 | // Test wrong level 35 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = 'WRONG'; 36 | expect(() => settings().toThrow()); 37 | 38 | delete process.env.SPLIT_EVALUATOR_LOG_LEVEL; 39 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = '{ "DEBUG": true }'; 40 | const global = settings(); 41 | expect(global).toHaveProperty('DEBUG', true); 42 | 43 | // Test INFO 44 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = 'INFO'; 45 | expect(() => settings().not.toThrow()); 46 | const info = settings(); 47 | expect(info).toHaveProperty('logLevel', 'INFO'); 48 | 49 | // Test WARN 50 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = 'WARN'; 51 | expect(() => settings().not.toThrow()); 52 | const warn = settings(); 53 | expect(warn).toHaveProperty('logLevel', 'WARN'); 54 | 55 | // Test ERROR 56 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = 'ERROR'; 57 | expect(() => settings().not.toThrow()); 58 | const error = settings(); 59 | expect(error).toHaveProperty('logLevel', 'ERROR'); 60 | 61 | // Test NONE 62 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = 'NONE'; 63 | expect(() => settings().not.toThrow()); 64 | const none = settings(); 65 | expect(none).toHaveProperty('logLevel', 'NONE'); 66 | 67 | // Test DEBUG 68 | process.env.SPLIT_EVALUATOR_LOG_LEVEL = 'DEBUG'; 69 | expect(() => settings().not.toThrow()); 70 | const debug = settings(); 71 | expect(debug).toHaveProperty('logLevel', 'DEBUG'); 72 | 73 | done(); 74 | }); 75 | 76 | test('ipAddressesEnabled', done => { 77 | // Test ok 78 | process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED = 'false'; 79 | expect(() => settings().not.toThrow()); 80 | 81 | const options = settings(); 82 | expect(options).toHaveProperty('core', { IPAddressesEnabled: false }); 83 | expect(options).not.toHaveProperty('impressionListener'); 84 | expect(options).not.toHaveProperty('scheduler'); 85 | expect(options).not.toHaveProperty('urls'); 86 | done(); 87 | }); 88 | 89 | test('listener', done => { 90 | // Test empty 91 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = ''; 92 | expect(() => settings().toThrow()); 93 | 94 | // Test trim 95 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = ' '; 96 | expect(() => settings().toThrow()); 97 | 98 | // Test ok 99 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = 'something'; 100 | expect(() => settings().toThrow()); 101 | 102 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = '1234567'; 103 | expect(() => settings().toThrow()); 104 | 105 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = 'http://localhost:1111/impressions'; 106 | 107 | process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED = null; 108 | 109 | const options = settings(); 110 | expect(options).toHaveProperty('impressionListener'); 111 | expect(options).not.toHaveProperty('scheduler'); 112 | expect(options).not.toHaveProperty('urls'); 113 | done(); 114 | }); 115 | 116 | test('scheduler', done => { 117 | // Test empty values 118 | process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE = ''; 119 | expect(() => settings().toThrow()); 120 | 121 | // Test trim 122 | process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE = '1'; 123 | process.env.SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE = ' '; 124 | expect(() => settings().toThrow()); 125 | 126 | // Test NaN 127 | process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE = '1'; 128 | process.env.SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE = '1'; 129 | process.env.SPLIT_EVALUATOR_METRICS_POST_RATE = 'asdf'; 130 | expect(() => settings().toThrow()); 131 | 132 | // Test empty 133 | process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE = '1'; 134 | process.env.SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE = '1'; 135 | process.env.SPLIT_EVALUATOR_METRICS_POST_RATE = '1'; 136 | process.env.SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE = ' '; 137 | expect(() => settings().toThrow()); 138 | 139 | // Test ok values 140 | process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE = '1'; 141 | process.env.SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE = '1'; 142 | process.env.SPLIT_EVALUATOR_METRICS_POST_RATE = '1'; 143 | process.env.SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE = '1'; 144 | process.env.SPLIT_EVALUATOR_EVENTS_POST_RATE = '1'; 145 | process.env.SPLIT_EVALUATOR_EVENTS_QUEUE_SIZE = '100'; 146 | expect(() => settings().toThrow()); 147 | 148 | process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT = 'https://127.0.0.1'; 149 | expect(() => settings().not.toThrow()); 150 | 151 | process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED = null; 152 | 153 | const options = settings(); 154 | expect(options).toHaveProperty('impressionListener'); 155 | expect(options).toHaveProperty('scheduler'); 156 | expect(options).toHaveProperty('scheduler.featuresRefreshRate', 1); 157 | expect(options).toHaveProperty('scheduler.segmentsRefreshRate', 1); 158 | expect(options).toHaveProperty('scheduler.metricsRefreshRate', 1); 159 | expect(options).toHaveProperty('scheduler.impressionsRefreshRate', 1); 160 | expect(options).toHaveProperty('scheduler.eventsPushRate', 1); 161 | expect(options).toHaveProperty('scheduler.eventsQueueSize', 100); 162 | done(); 163 | }); 164 | 165 | test('globalConfig', done => { 166 | delete process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE; 167 | delete process.env.SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE; 168 | delete process.env.SPLIT_EVALUATOR_METRICS_POST_RATE; 169 | delete process.env.SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE; 170 | delete process.env.SPLIT_EVALUATOR_EVENTS_POST_RATE; 171 | delete process.env.SPLIT_EVALUATOR_EVENTS_QUEUE_SIZE; 172 | 173 | // null or empty 174 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = null; 175 | expect(() => settings()).toThrow(); 176 | 177 | // // is string 178 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = {'debug': true}; 179 | expect(() => settings()).toThrow(); 180 | 181 | // Test core property cleanning 182 | process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG = JSON.stringify({ 183 | core: core, 184 | scheduler: scheduler, 185 | urls: urls, 186 | storage: storage, 187 | startup: startup, 188 | sync: sync, 189 | mode: 'consumer', 190 | debug: true, 191 | streamingEnabled: false, 192 | integrations: integrations, 193 | }); 194 | let config = settings(); 195 | 196 | // core should be deleted to use environment configurations 197 | expect(config.core).toEqual({}); 198 | expect(config.scheduler).toEqual(scheduler); 199 | expect(config.urls).toEqual(urls); 200 | // storage should be deleted to use default in memory 201 | expect(config.storage).toEqual(undefined); 202 | expect(config.startup).toEqual(startup); 203 | // should avoid sync.enabled property to use default false 204 | sync.enabled = undefined; 205 | expect(config.sync).toEqual(sync); 206 | // should avoid mode property to use default standalone 207 | expect(config.mode).toEqual(undefined); 208 | expect(config.debug).toBe(true); 209 | expect(config.streamingEnabled).toBe(false); 210 | // integrations config should be avoided 211 | expect(config.integrations).toEqual(undefined); 212 | 213 | // scheduler evaluator configs should be priorized over global configs 214 | process.env.SPLIT_EVALUATOR_SPLITS_REFRESH_RATE = 2; 215 | process.env.SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE = 2; 216 | process.env.SPLIT_EVALUATOR_METRICS_POST_RATE = 2; 217 | process.env.SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE = 2; 218 | process.env.SPLIT_EVALUATOR_EVENTS_POST_RATE = 2; 219 | process.env.SPLIT_EVALUATOR_EVENTS_QUEUE_SIZE = 2; 220 | 221 | // url evaluator configs should be priorized over global configs 222 | process.env.SPLIT_EVALUATOR_SDK_URL = 'https://sdk.env-split.io/api'; 223 | process.env.SPLIT_EVALUATOR_EVENTS_URL = 'https://events.env-split.io/api'; 224 | process.env.SPLIT_EVALUATOR_AUTH_SERVICE_URL = 'https://auth.env-split.io/api'; 225 | process.env.SPLIT_EVALUATOR_TELEMETRY_URL = 'https://telemetry.env-split.io/api'; 226 | 227 | // IP Addresses enabled evaluator configs should be priorized over global configs 228 | process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED = 'false'; 229 | 230 | config = settings(); 231 | expect(config.scheduler).toEqual({ 232 | eventsPushRate: 2, 233 | eventsQueueSize: 2, 234 | featuresRefreshRate: 2, 235 | impressionsQueueSize: 7, 236 | impressionsRefreshRate: 2, 237 | segmentsRefreshRate: 2, 238 | metricsRefreshRate: 2, 239 | }); 240 | expect(config.core.IPAddressesEnabled).toBe(false); 241 | expect(config.urls).toEqual({ 242 | 'sdk': process.env.SPLIT_EVALUATOR_SDK_URL, 243 | 'events': process.env.SPLIT_EVALUATOR_EVENTS_URL, 244 | 'auth': process.env.SPLIT_EVALUATOR_AUTH_SERVICE_URL, 245 | 'streaming': config.urls.streaming, 246 | 'telemetry': process.env.SPLIT_EVALUATOR_TELEMETRY_URL, 247 | }); 248 | 249 | done(); 250 | }); 251 | 252 | }); 253 | -------------------------------------------------------------------------------- /utils/parserConfigs/__tests__/validators.test.js: -------------------------------------------------------------------------------- 1 | const { nullOrEmpty, parseNumber, validUrl, validLogLevel } = require('../validators'); 2 | 3 | describe('validators', () => { 4 | test('nullOrEmpty', done => { 5 | expect(() => nullOrEmpty().toThrow()); 6 | expect(() => nullOrEmpty('').toThrow()); 7 | expect(() => nullOrEmpty(' ').toThrow()); 8 | expect(() => nullOrEmpty('a').not.toThrow()); 9 | done(); 10 | }); 11 | 12 | test('parseNumber', done => { 13 | expect(() => parseNumber().toThrow()); 14 | expect(() => parseNumber('').toThrow()); 15 | expect(() => parseNumber(' ').toThrow()); 16 | expect(() => parseNumber('a').toThrow()); 17 | expect(() => parseNumber('123').not.toThrow()); 18 | done(); 19 | }); 20 | 21 | test('validUrl', done => { 22 | expect(() => validUrl().toThrow()); 23 | expect(() => validUrl('').toThrow()); 24 | expect(() => validUrl(' ').toThrow()); 25 | expect(() => validUrl('a').toThrow()); 26 | expect(() => validUrl('123').not.toThrow()); 27 | expect(() => validUrl('http://123.123.123:1234').not.toThrow()); 28 | expect(() => validUrl('http://localhost').not.toThrow()); 29 | expect(() => validUrl('http://my-app_src1/imp').not.toThrow()); 30 | expect(() => validUrl('http://localhost/imp').not.toThrow()); 31 | expect(() => validUrl('https://123.123.123:1234').not.toThrow()); 32 | expect(() => validUrl('https://123.123.123:1234/imp').not.toThrow()); 33 | expect(() => validUrl('www.test.com').not.toThrow()); 34 | expect(() => validUrl('www.test.com:12345').not.toThrow()); 35 | expect(() => validUrl('www.test.com:12345/impr').not.toThrow()); 36 | expect(() => validUrl('www.test.com/impr').not.toThrow()); 37 | expect(() => validUrl('www.test-myapp.com/impr').not.toThrow()); 38 | done(); 39 | }); 40 | 41 | test('validLogLevel', done => { 42 | expect(() => validLogLevel().not.toThrow()); 43 | expect(() => validLogLevel('').toThrow()); 44 | expect(() => validLogLevel(' ').toThrow()); 45 | expect(() => validLogLevel('a ').toThrow()); 46 | expect(() => validLogLevel('INFO').not.toThrow()); 47 | expect(() => validLogLevel('info ').not.toThrow()); 48 | expect(() => validLogLevel('WARN').not.toThrow()); 49 | expect(() => validLogLevel('warn').not.toThrow()); 50 | expect(() => validLogLevel('ERROR').not.toThrow()); 51 | expect(() => validLogLevel('NONE').not.toThrow()); 52 | expect(() => validLogLevel('DEBUG').not.toThrow()); 53 | done(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /utils/parserConfigs/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { parseNumber, validUrl, validLogLevel, validGlobalConfig, throwError } = require('./validators'); 3 | 4 | const getConfigs = () => { 5 | let configs = { 6 | features: path.join(__dirname, 'split.yml'), 7 | core: {}, 8 | }; 9 | 10 | const nulleableConfigs = { 11 | core: undefined, 12 | storage: undefined, 13 | mode: undefined, 14 | integrations: undefined, 15 | }; 16 | 17 | if (process.env.SPLIT_EVALUATOR_GLOBAL_CONFIG) { 18 | console.info('Setting global config'); 19 | const globalConfig = validGlobalConfig('SPLIT_EVALUATOR_GLOBAL_CONFIG'); 20 | 21 | if (process.env.SPLIT_EVALUATOR_ENVIRONMENTS){ 22 | if (globalConfig.sync && globalConfig.sync.splitFilters) { 23 | throwError('Flag sets must be defined in SPLIT_EVALUATOR_ENVIRONMENTS, initialization aborted'); 24 | } 25 | } 26 | 27 | configs = Object.assign(globalConfig, nulleableConfigs, configs); 28 | if (configs.sync) Object.assign(configs.sync, {enabled: undefined} ); 29 | 30 | } 31 | 32 | // LOG LEVEL 33 | const logLevel = validLogLevel('SPLIT_EVALUATOR_LOG_LEVEL'); 34 | if (logLevel) { 35 | configs.logLevel = logLevel; 36 | } 37 | 38 | // SCHEDULER OPTIONS 39 | const scheduler = {}; 40 | 41 | const featuresRefreshRate = parseNumber('SPLIT_EVALUATOR_SPLITS_REFRESH_RATE'); 42 | if (featuresRefreshRate) scheduler.featuresRefreshRate = featuresRefreshRate; 43 | 44 | const segmentsRefreshRate = parseNumber('SPLIT_EVALUATOR_SEGMENTS_REFRESH_RATE'); 45 | if (segmentsRefreshRate) scheduler.segmentsRefreshRate = segmentsRefreshRate; 46 | 47 | const metricsRefreshRate = parseNumber('SPLIT_EVALUATOR_METRICS_POST_RATE'); 48 | if (metricsRefreshRate) scheduler.metricsRefreshRate = metricsRefreshRate; 49 | 50 | const impressionsRefreshRate = parseNumber('SPLIT_EVALUATOR_IMPRESSIONS_POST_RATE'); 51 | if (impressionsRefreshRate) scheduler.impressionsRefreshRate = impressionsRefreshRate; 52 | 53 | const eventsPushRate = parseNumber('SPLIT_EVALUATOR_EVENTS_POST_RATE'); 54 | if (eventsPushRate) scheduler.eventsPushRate = eventsPushRate; 55 | 56 | const eventsQueueSize = parseNumber('SPLIT_EVALUATOR_EVENTS_QUEUE_SIZE'); 57 | if (eventsQueueSize) scheduler.eventsQueueSize = eventsQueueSize; 58 | 59 | // URLS 60 | const urls = {}; 61 | const events = process.env.SPLIT_EVALUATOR_EVENTS_URL; 62 | if (events) urls.events = events; 63 | const sdk = process.env.SPLIT_EVALUATOR_SDK_URL; 64 | if (sdk) urls.sdk = sdk; 65 | const auth = process.env.SPLIT_EVALUATOR_AUTH_SERVICE_URL; 66 | if (auth) urls.auth = auth; 67 | const telemetry = process.env.SPLIT_EVALUATOR_TELEMETRY_URL; 68 | if (telemetry) urls.telemetry = telemetry; 69 | 70 | // IMPRESSION LISTENER 71 | if (process.env.SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT && validUrl('SPLIT_EVALUATOR_IMPRESSION_LISTENER_ENDPOINT')) { 72 | console.log('Setting impression listener.'); 73 | const impressionListener = require('../../listener/'); 74 | configs.impressionListener = impressionListener; 75 | } 76 | 77 | // IP ADDRESS ENABLED 78 | if (process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED && process.env.SPLIT_EVALUATOR_IP_ADDRESSES_ENABLED.toLowerCase() === 'false') { 79 | configs.core.IPAddressesEnabled = false; 80 | } 81 | 82 | if (Object.keys(scheduler).length > 0) { 83 | console.log('Setting custom SDK scheduler timers.'); 84 | // merge global and environment config priorizing environment variables 85 | configs.scheduler = configs.scheduler ? Object.assign(configs.scheduler, scheduler) : scheduler; 86 | } 87 | 88 | if (Object.keys(urls).length > 0) { 89 | console.log('Setting custom urls.'); 90 | // merge global and environment config priorizing environment variables 91 | configs.urls = configs.urls ? Object.assign(configs.urls, urls) : urls; 92 | } 93 | 94 | return configs; 95 | }; 96 | 97 | module.exports = getConfigs; -------------------------------------------------------------------------------- /utils/parserConfigs/split.yml: -------------------------------------------------------------------------------- 1 | - my-experiment: 2 | treatment: "on" 3 | keys: "test" 4 | config: "{\"desc\" : \"this applies only to ON treatment\"}" 5 | - other-experiment-3: 6 | treatment: "off" 7 | - my-experiment: 8 | treatment: "off" 9 | keys: "only_test" 10 | config: "{\"desc\" : \"this applies only to OFF and only for only_test. The rest will receive ON\"}" 11 | - other-experiment-3: 12 | treatment: "on" 13 | keys: "test_whitelist" 14 | - other-experiment: 15 | treatment: "on" 16 | keys: ["test2","test3"] 17 | - other-experiment-2: 18 | treatment: "on" -------------------------------------------------------------------------------- /utils/parserConfigs/validators.js: -------------------------------------------------------------------------------- 1 | const throwError = (msg) => { 2 | console.log(msg); 3 | throw new Error(msg); 4 | }; 5 | 6 | const validUrl = (name) => { 7 | const url = process.env[name]; 8 | if (/^(https?:\/\/)?((([\da-zA-Z-_]+)(\.([\da-zA-Z-_]+))*)|(\d+\.\d+.\d+.\d+))(:\d{1,5})?(\/.*)?$/.test(url)) { 9 | return url; 10 | } 11 | throwError(`you passed an invalid url for ${name}. Received "${url}".`); 12 | }; 13 | 14 | const isUndefined = (name) => { 15 | const input = process.env[name]; 16 | // eslint-disable-next-line eqeqeq 17 | if (input == undefined) throwError(`you passed a null or undefined ${name}, ${name} must be a non-empty string.`); 18 | return input; 19 | }; 20 | 21 | const isEmpty = (name) => { 22 | const trimmed = process.env[name].trim(); 23 | if (trimmed.length === 0) throwError(`you passed an empty ${name}, ${name} must be a non-empty string.`); 24 | return trimmed; 25 | }; 26 | 27 | const nullOrEmpty = (name) => { 28 | isUndefined(name); 29 | return isEmpty(name); 30 | }; 31 | 32 | const parseNumber = (name) => { 33 | const input = process.env[name]; 34 | 35 | // eslint-disable-next-line eqeqeq 36 | if (input == undefined) return null; 37 | 38 | const trimmed = isEmpty(name); 39 | const inputNumber = Number(trimmed); 40 | if (isNaN(inputNumber)) throwError(`you passed an invalid ${name}, ${name} must be a valid number.`); 41 | return inputNumber; 42 | }; 43 | 44 | const validLogLevel = (name) => { 45 | const input = process.env[name]; 46 | 47 | // eslint-disable-next-line eqeqeq 48 | if (input == undefined) return null; 49 | 50 | const logLevel = isEmpty(name).toUpperCase(); 51 | const validLevels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE']; 52 | if (validLevels.includes(logLevel)) return logLevel; 53 | throwError(`you passed ${logLevel} is an invalid log level, ${name} accepts NONE|DEBUG|INFO|WARN|ERROR`); 54 | }; 55 | 56 | const isString = (value) => { 57 | return (typeof value === 'string' || value instanceof String); 58 | }; 59 | 60 | const validEnvironment = (environment) => { 61 | if(!environment['API_KEY']) throwError('API_KEY value not present in one or more environment config'); 62 | if(!environment['AUTH_TOKEN']) throwError('AUTH_TOKEN value not present in one or more environment config'); 63 | }; 64 | 65 | const validEnvironmentConfig = (environmentParam) => { 66 | nullOrEmpty(environmentParam); 67 | const input = process.env[environmentParam]; 68 | if (!isString(input)) 69 | throwError(`you passed an invalid ${environmentParam}, ${environmentParam} must be a string`); 70 | const environmentConfig = JSON.parse(process.env[environmentParam]); 71 | if(!Array.isArray(environmentConfig)) 72 | throwError(`you passed an invalid ${environmentParam}, ${environmentParam} must be a list of environments.`); 73 | return environmentConfig; 74 | }; 75 | 76 | const validGlobalConfig = (globalParam) => { 77 | nullOrEmpty(globalParam); 78 | const input = process.env[globalParam]; 79 | if (!isString(input)) 80 | throwError(`you passed an invalid ${globalParam}, ${globalParam} must be a string`); 81 | try { 82 | const globalConfig = JSON.parse(process.env[globalParam]); 83 | return globalConfig; 84 | } catch (err) { 85 | throwError('Invalid globalConfig JSON'); 86 | } 87 | }; 88 | 89 | const validFlagSets = (maybeFlagSets) => { 90 | if (!maybeFlagSets) return; 91 | if (!isString(maybeFlagSets)) { 92 | throwError('you passed an invalid flag set, flag sets must be comma separated a string list'); 93 | return; 94 | } 95 | return [{type: 'bySet', values: maybeFlagSets.split(',')}]; 96 | }; 97 | 98 | module.exports = { 99 | throwError, 100 | validUrl, 101 | validLogLevel, 102 | nullOrEmpty, 103 | parseNumber, 104 | isString, 105 | validEnvironment, 106 | validEnvironmentConfig, 107 | validGlobalConfig, 108 | validFlagSets, 109 | }; -------------------------------------------------------------------------------- /utils/split1.yml: -------------------------------------------------------------------------------- 1 | # Always blue 2 | - testing_split_blue: 3 | treatment: "blue" 4 | - testing_split_color: 5 | treatment: "blue" 6 | # This one will (or should) return control for non specified keys on whitelist 7 | - testing_split_only_wl: 8 | treatment: "whitelisted" 9 | keys: ["blue"] 10 | # Playing with whitelists 11 | - testing_split_with_wl: 12 | treatment: "not_in_whitelist" 13 | config: "{\"color\": \"green\"}" 14 | - testing_split_with_wl: 15 | treatment: "one_key_wl" 16 | keys: "key_blue" 17 | - testing_split_with_wl: 18 | treatment: "multi_key_wl" 19 | keys: ["blue"] 20 | config: "{\"color\": \"red\"}" 21 | # All keys with config 22 | - testing_split_with_config: 23 | treatment: "blue" 24 | config: "{\"color\": \"blue\"}" 25 | -------------------------------------------------------------------------------- /utils/split2.yml: -------------------------------------------------------------------------------- 1 | # Always red 2 | - testing_split_red: 3 | treatment: "red" 4 | - testing_split_color: 5 | treatment: "red" 6 | # This one will (or should) return control for non specified keys on whitelist 7 | - testing_split_only_wl: 8 | treatment: "whitelisted" 9 | keys: ["red"] 10 | # Playing with whitelists 11 | - testing_split_with_wl: 12 | treatment: "not_in_whitelist" 13 | config: "{\"color\": \"green\"}" 14 | - testing_split_with_wl: 15 | treatment: "one_key_wl" 16 | keys: "key_for_wl" 17 | - testing_split_with_wl: 18 | treatment: "multi_key_wl" 19 | keys: ["key_for_wl_1", "key_for_wl_2"] 20 | config: "{\"color\": \"brown\"}" 21 | # All keys with config 22 | - testing_split_with_config: 23 | treatment: "red" 24 | config: "{\"color\": \"red\"}" 25 | -------------------------------------------------------------------------------- /utils/split3.yml: -------------------------------------------------------------------------------- 1 | # Always green 2 | - testing_split_green: 3 | treatment: "green" 4 | - testing_split_color: 5 | treatment: "green" 6 | # All keys with config 7 | - testing_split_with_config: 8 | treatment: "green" 9 | config: "{\"color\": \"green\"}" 10 | -------------------------------------------------------------------------------- /utils/split4.yml: -------------------------------------------------------------------------------- 1 | # Always purple 2 | - testing_split_purple: 3 | treatment: "purple" 4 | - testing_split_color: 5 | treatment: "purple" 6 | # All keys with config 7 | - testing_split_with_config: 8 | treatment: "purple" 9 | config: "{\"color\": \"purple\"}" 10 | -------------------------------------------------------------------------------- /utils/testWrapper/index.js: -------------------------------------------------------------------------------- 1 | const expectError = (response, code, message) => { 2 | expect(response.statusCode).toBe(code); 3 | expect(response.body).toHaveProperty('error'); 4 | if (message) expect(response.body.error).toBe(message); 5 | }; 6 | 7 | const expectErrorContaining = (response, code, message) => { 8 | expect(response.statusCode).toBe(code); 9 | expect(response.body).toHaveProperty('error'); 10 | expect(response.body.error).toEqual(expect.arrayContaining(message)); 11 | }; 12 | 13 | const expectOk = (response, code, treatmentResult, featureFlag, config) => { 14 | expect(response.statusCode).toBe(code); 15 | expect(response.body).toHaveProperty('treatment', treatmentResult); 16 | expect(response.body).toHaveProperty('splitName', featureFlag); 17 | // eslint-disable-next-line eqeqeq 18 | if (config != undefined) { 19 | expect(response.body).toHaveProperty('config', config); 20 | } 21 | }; 22 | 23 | const expectOkMultipleResults = (response, code, expectedTreatment, expectedLength) => { 24 | expect(response.statusCode).toBe(code); 25 | // Check length 26 | const featureFlagNames = Object.keys(response.body); 27 | expect(featureFlagNames.length).toEqual(expectedLength); 28 | // Iterate over featureFlags 29 | const featureFlags = Object.keys(expectedTreatment); 30 | featureFlags.forEach(featureFlag => { 31 | if (response.body[featureFlag].treatment) { 32 | expect(response.body[featureFlag].treatment).toEqual(expectedTreatment[featureFlag].treatment); 33 | } else { 34 | expect(response.body[featureFlag]).toEqual(expectedTreatment[featureFlag].treatment); 35 | } 36 | // eslint-disable-next-line no-prototype-builtins 37 | if (expectedTreatment[featureFlag].hasOwnProperty('config')) { 38 | expect(response.body[featureFlag].config).toEqual(expectedTreatment[featureFlag].config); 39 | } 40 | }); 41 | }; 42 | 43 | const expectOkAllTreatments = (response, code, expectedTreatments, expectedLength) => { 44 | expect(response.statusCode).toBe(code); 45 | expect(Object.keys(response.body).length).toEqual(expectedLength); 46 | expect(response.body).toEqual(expectedTreatments); 47 | }; 48 | 49 | const getLongKey = () => { 50 | let key = ''; 51 | for (let i = 0; i <=250; i++) { 52 | key += 'a'; 53 | } 54 | return key; 55 | }; 56 | 57 | const gracefulShutDown = async () => { 58 | 59 | const environmentManagerFactory = require('../../environmentManager'); 60 | if (environmentManagerFactory.hasInstance()) { 61 | await environmentManagerFactory.destroy(); 62 | } 63 | 64 | const impressionManagerFactory = require('../../listener/manager'); 65 | if (impressionManagerFactory.hasInstance()) { 66 | await impressionManagerFactory.destroy(); 67 | } 68 | }; 69 | 70 | module.exports = { 71 | expectError, 72 | expectErrorContaining, 73 | expectOk, 74 | expectOkAllTreatments, 75 | expectOkMultipleResults, 76 | getLongKey, 77 | gracefulShutDown, 78 | }; -------------------------------------------------------------------------------- /utils/utils.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('../package.json'); 2 | 3 | let serverUpSince = null; 4 | 5 | /** 6 | * getVersion returns current version 7 | */ 8 | const getVersion = () => packageJson && packageJson.version; 9 | 10 | /** 11 | * toHHMMSS parses time 12 | * @param {Number} ms 13 | */ 14 | const toHHMMSS = (ms) => { 15 | let secNum = ms / 1000; // Transform to seconds for easier numbers, as we expect millis. 16 | // And after each count, we remove the counted ammount from secNum; 17 | const days = Math.floor(secNum / 86400); secNum -= days * 86400; 18 | const hours = Math.floor(secNum / 3600); secNum -= hours * 3600; 19 | const minutes = Math.floor(secNum / 60); secNum -= minutes * 60; 20 | 21 | return `${days}d ${hours}h ${minutes}m ${Math.round(secNum)}s`; 22 | }; 23 | 24 | /** 25 | * uptime Wether we need to initialize the timer. If it's falsey, return the current uptime. 26 | * @param {boolean} init 27 | */ 28 | const uptime = init => { 29 | if (init) { 30 | serverUpSince = Date.now(); 31 | } else { 32 | return toHHMMSS(Date.now() - serverUpSince); 33 | } 34 | }; 35 | 36 | /** 37 | * parseValidators Grabs each validator and merge the message to be displayed. 38 | * @param {Array} validators 39 | */ 40 | const parseValidators = (validators) => { 41 | const errors = []; 42 | validators.forEach(validator => { 43 | if (validator && !validator.valid) errors.push(validator.error); 44 | }); 45 | return errors; 46 | }; 47 | 48 | /** 49 | * parseValidators Replace all characters with '#' leaving last 4 50 | * @param {Array} validators 51 | */ 52 | const obfuscate = (value) => { 53 | return value.replace(/.(?=.{4,}$)/g, '#'); 54 | }; 55 | 56 | 57 | module.exports = { 58 | getVersion, 59 | uptime, 60 | parseValidators, 61 | __dirname, 62 | obfuscate, 63 | }; 64 | --------------------------------------------------------------------------------