├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS └── workflows │ ├── common-js-ci.yml │ └── stale.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEPENDENCIES.md ├── DEPLOY.md ├── LICENSE ├── README.md ├── deploy.sh ├── package-lock.json ├── package.json ├── samples └── deno-sandbox │ ├── .vscode │ ├── launch.json │ └── settings.json │ ├── deno.json │ ├── fake-config-fetcher.ts │ ├── import_map.json │ ├── main.ts │ └── sample.json ├── src ├── AutoPollConfigService.ts ├── ConfigCatCache.ts ├── ConfigCatClient.ts ├── ConfigCatClientOptions.ts ├── ConfigCatLogger.ts ├── ConfigFetcher.ts ├── ConfigJson.ts ├── ConfigServiceBase.ts ├── DefaultEventEmitter.ts ├── EvaluateLogBuilder.ts ├── EventEmitter.ts ├── FlagOverrides.ts ├── Hash.ts ├── Hooks.ts ├── LazyLoadConfigService.ts ├── ManualPollConfigService.ts ├── Polyfills.ts ├── ProjectConfig.ts ├── RolloutEvaluator.ts ├── Semver.ts ├── User.ts ├── Utils.ts ├── index.ts ├── lib.es2021.promise.d.ts └── lib.es2021.weakref.d.ts ├── test ├── ConfigCatCacheTests.ts ├── ConfigCatClientCacheTests.ts ├── ConfigCatClientOptionsTests.ts ├── ConfigCatClientTests.ts ├── ConfigCatLoggerTests.ts ├── ConfigServiceBaseTests.ts ├── ConfigV2EvaluationTests.ts ├── DataGovernanceTests.ts ├── DefaultEventEmitterTests.ts ├── DefaultUserTests.ts ├── EvaluationLogTests.ts ├── HashTests.ts ├── IndexTests.ts ├── MatrixTests.ts ├── OverrideTests.ts ├── PolyfillTests.ts ├── ProjectConfigTests.ts ├── UserTests.ts ├── UtilsTests.ts ├── VariationIdTests.ts ├── data │ ├── comparison_attribute_conversion.json │ ├── comparison_attribute_trimming.json │ ├── comparison_value_trimming.json │ ├── evaluationlog │ │ ├── 1_targeting_rule.json │ │ ├── 1_targeting_rule │ │ │ ├── 1_rule_matching_targeted_attribute.txt │ │ │ ├── 1_rule_no_targeted_attribute.txt │ │ │ ├── 1_rule_no_user.txt │ │ │ └── 1_rule_not_matching_targeted_attribute.txt │ │ ├── 2_targeting_rules.json │ │ ├── 2_targeting_rules │ │ │ ├── 2_rules_matching_targeted_attribute.txt │ │ │ ├── 2_rules_no_targeted_attribute.txt │ │ │ ├── 2_rules_no_user.txt │ │ │ └── 2_rules_not_matching_targeted_attribute.txt │ │ ├── _overrides │ │ │ └── test_list_truncation.json │ │ ├── and_rules.json │ │ ├── and_rules │ │ │ ├── and_rules_no_user.txt │ │ │ └── and_rules_user.txt │ │ ├── comparators.json │ │ ├── comparators │ │ │ └── allinone.txt │ │ ├── epoch_date_validation.json │ │ ├── epoch_date_validation │ │ │ └── date_error.txt │ │ ├── list_truncation.json │ │ ├── list_truncation │ │ │ └── list_truncation.txt │ │ ├── number_validation.json │ │ ├── number_validation │ │ │ └── number_error.txt │ │ ├── options_after_targeting_rule.json │ │ ├── options_after_targeting_rule │ │ │ ├── options_after_targeting_rule_matching_targeted_attribute.txt │ │ │ ├── options_after_targeting_rule_no_targeted_attribute.txt │ │ │ ├── options_after_targeting_rule_no_user.txt │ │ │ └── options_after_targeting_rule_not_matching_targeted_attribute.txt │ │ ├── options_based_on_custom_attr.json │ │ ├── options_based_on_custom_attr │ │ │ ├── matching_options_custom_attribute.txt │ │ │ ├── no_options_custom_attribute.txt │ │ │ └── options_custom_attribute_no_user.txt │ │ ├── options_based_on_user_id.json │ │ ├── options_based_on_user_id │ │ │ ├── options_user_attribute_no_user.txt │ │ │ └── options_user_attribute_user.txt │ │ ├── options_within_targeting_rule.json │ │ ├── options_within_targeting_rule │ │ │ ├── options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt │ │ │ ├── options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt │ │ │ ├── options_within_targeting_rule_no_targeted_attribute.txt │ │ │ ├── options_within_targeting_rule_no_user.txt │ │ │ └── options_within_targeting_rule_not_matching_targeted_attribute.txt │ │ ├── prerequisite_flag.json │ │ ├── prerequisite_flag │ │ │ ├── prerequisite_flag.txt │ │ │ ├── prerequisite_flag_multilevel.txt │ │ │ ├── prerequisite_flag_no_user_needed_by_both.txt │ │ │ ├── prerequisite_flag_no_user_needed_by_dep.txt │ │ │ └── prerequisite_flag_no_user_needed_by_prereq.txt │ │ ├── segment.json │ │ ├── segment │ │ │ ├── segment_matching.txt │ │ │ ├── segment_no_matching.txt │ │ │ ├── segment_no_targeted_attribute.txt │ │ │ ├── segment_no_user.txt │ │ │ └── segment_no_user_multi_conditions.txt │ │ ├── semver_validation.json │ │ ├── semver_validation │ │ │ ├── semver_error.txt │ │ │ └── semver_relations_error.txt │ │ ├── simple_value.json │ │ └── simple_value │ │ │ ├── double_setting.txt │ │ │ ├── int_setting.txt │ │ │ ├── off_flag.txt │ │ │ ├── on_flag.txt │ │ │ └── text_setting.txt │ ├── test_circulardependency_v6.json │ ├── testmatrix.csv │ ├── testmatrix_and_or.csv │ ├── testmatrix_comparators_v6.csv │ ├── testmatrix_number.csv │ ├── testmatrix_prerequisite_flag.csv │ ├── testmatrix_segments.csv │ ├── testmatrix_segments_old.csv │ ├── testmatrix_semantic.csv │ ├── testmatrix_semantic_2.csv │ ├── testmatrix_sensitive.csv │ ├── testmatrix_unicode.csv │ └── testmatrix_variationid.csv └── helpers │ ├── ConfigCatClientCacheExtensions.ts │ ├── ConfigLocation.ts │ ├── HttpConfigFetcher.ts │ ├── fakes.ts │ └── utils.ts ├── tsconfig.build.cjs.json ├── tsconfig.build.esm.json ├── tsconfig.build.json ├── tsconfig.json └── tsconfig.mocha.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | coverage/ 3 | dist/ 4 | lib/ 5 | node_modules/ 6 | src/Semver.ts 7 | src/Hash.ts 8 | src/lib.*.d.ts -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @configcat/developers 2 | -------------------------------------------------------------------------------- /.github/workflows/common-js-ci.yml: -------------------------------------------------------------------------------- 1 | name: Common JS CI 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | branches: [ master ] 8 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | node: [ 14, 16, 18, 20 ] 20 | os: [ macos-latest, ubuntu-latest, windows-latest ] 21 | exclude: 22 | - node: 14 23 | os: macos-latest 24 | fail-fast: false 25 | name: Test [${{ matrix.os }}, Node ${{ matrix.node }}] 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node }} 32 | 33 | - name: Install dependencies 34 | run: | 35 | npm install 36 | 37 | - name: Build 38 | run: npm run build 39 | 40 | - name: Test 41 | run: npm run test 42 | 43 | - name: Archive 44 | if: failure() 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: checked-out-lib 48 | path: lib 49 | 50 | coverage: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v3 54 | 55 | - uses: actions/setup-node@v3 56 | with: 57 | node-version: 14 58 | 59 | - name: Install dependencies 60 | run: | 61 | npm install codecov -g 62 | npm install 63 | 64 | - name: Build 65 | run: npm run build 66 | 67 | - name: Coverage 68 | run: npm run coverage 69 | 70 | - name: Upload coverage report 71 | run: codecov 72 | 73 | publish: 74 | needs: [ test, coverage ] 75 | runs-on: ubuntu-latest 76 | if: startsWith(github.ref, 'refs/tags') 77 | steps: 78 | - uses: actions/checkout@v3 79 | 80 | - uses: actions/setup-node@v3 81 | with: 82 | node-version: 14 83 | registry-url: 'https://registry.npmjs.org' 84 | 85 | - name: Install dependencies 86 | run: npm install 87 | 88 | - name: 🚀Publish 89 | run: npm publish 90 | env: 91 | NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }} 92 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * *' 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | stale: 11 | uses: configcat/.github/.github/workflows/stale.yml@master 12 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | lib/ 61 | .idea/ 62 | 63 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | .vscode/ 3 | node_modules/ 4 | src/ 5 | samples/ 6 | test/ 7 | coverage/ 8 | package-lock.json 9 | tsconfig*.json 10 | *.tgz 11 | .nyc_output/ 12 | .idea/ 13 | *.log 14 | deploy.sh 15 | tslint.json 16 | DEPENDENCIES.md 17 | DEPLOY.md 18 | .github/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions which should be recommended for users of this workspace. 3 | "recommendations": [ 4 | "dbaeumer.vscode-eslint" 5 | ], 6 | 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [] 9 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run tests", 8 | "cwd": "${workspaceRoot}", 9 | "env": { "TS_NODE_PROJECT": "./tsconfig.mocha.json" }, 10 | "runtimeArgs": [ 11 | "--expose-gc" 12 | ], 13 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 14 | "args": [ 15 | "--require", 16 | "ts-node/register", 17 | "test/**/*.ts", 18 | "--fgrep", 19 | "", 20 | "--color", 21 | "--exit", 22 | "--timeout", 23 | "30000" 24 | ], 25 | "internalConsoleOptions": "openOnSessionStart", 26 | "sourceMaps": true, 27 | "outFiles": [ 28 | "${workspaceFolder}/**/*.js", 29 | "!${workspaceFolder}/**/node_modules/**" 30 | ], 31 | "skipFiles": [ 32 | "/**/*.js" 33 | ] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [160], 3 | "[typescript]": { 4 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 5 | }, 6 | "eslint.format.enable": true, 7 | "eslint.validate": [ 8 | "typescript" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Please check the [Github Releases](https://github.com/configcat/common-js/releases) page for the changelog of the ConfigCat Common library for JavaScript. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the ConfigCat Common library for JavaScript 2 | 3 | ConfigCat SDK is an open source project. Feedback and contribution are welcome. Contributions are made to this repo via Issues and Pull Requests. 4 | 5 | ## Submitting bug reports and feature requests 6 | 7 | The ConfigCat SDK team monitors the [issue tracker](https://github.com/configcat/common-js/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The team will respond to all newly filed issues. 8 | 9 | ## Submitting pull requests 10 | 11 | We encourage pull requests and other contributions from the community. 12 | - Before submitting pull requests, ensure that all temporary or unintended code is removed. 13 | - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). 14 | - Add unit or integration tests for fixed or changed functionality. 15 | 16 | When you submit a pull request or otherwise seek to include your change in the repository, you waive all your intellectual property rights, including your copyright and patent claims for the submission. For more details please read the [contribution agreement](https://github.com/configcat/legal/blob/main/contribution-agreement.md). 17 | 18 | In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 19 | 20 | 1. Fork the repository to your own Github account 21 | 2. Clone the project to your machine 22 | 3. Create a branch locally with a succinct but descriptive name 23 | 4. Commit changes to the branch 24 | 5. Following any formatting and testing guidelines specific to this repo 25 | 6. Push changes to your fork 26 | 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 27 | 28 | ## Build instructions 29 | 30 | The project uses [npm](https://www.npmjs.com) for dependency management. 31 | 32 | To install dependencies: 33 | 34 | ```bash 35 | npm install 36 | ``` 37 | 38 | ## Running tests 39 | 40 | ```bash 41 | npm test 42 | ``` 43 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # How to update dependencies (security) 2 | 3 | 1. `npm audit fix` 4 | 1. If there were fixes, create a pull request with the changes 5 | 1. If necessary, [Deploy](DEPLOY.md) a new version. -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # How to deploy 2 | 3 | ## Before deployment 4 | 5 | Make sure the CI is running: https://github.com/configcat/common-js/actions/workflows/common-js-ci.yml 6 | 7 | ## Via shell script 8 | 9 | 1. Run `./deploy.sh` 10 | 11 | 2. Add release notes: https://github.com/configcat/common-js/releases 12 | 13 | 2. Update `common-js` in `js-sdk` and `node-sdk` and `js-ssr-sdk` and re-deploy both packages. 14 | 15 | 3. Test all packages manually! 16 | 17 | ### or 18 | 19 | ## Manually 20 | 1. Run tests 21 | ```PowerShell 22 | npm test 23 | ``` 24 | 25 | 1. Create a new version (patch, minor, major) 26 | Increase version number by using `npm version patch | minor | major` 27 | 28 | *Example: increasing patch version* 29 | ```PowerShell 30 | npm version patch 31 | ``` 32 | 33 | 1. Push tag to remote 34 | 35 | If you tag the commit, a GitHub action automatically publishes the package to NPM. 36 | ```PowerShell 37 | git push origin 38 | ``` 39 | *Example: git push origin v1.1.15* 40 | 41 | You can follow the build status [here](https://github.com/configcat/common-js/actions/workflows/common-js-ci.yml). 42 | 43 | 2. Add release notes: https://github.com/configcat/common-js/releases 44 | 2. Update `common-js` in `js-sdk`, `node-sdk` and `js-ssr-sdk` and re-deploy all packages. 45 | 3. Test all packages manually! 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 configcat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat Common library for JavaScript 2 | 3 | [![Common JS CI](https://github.com/configcat/common-js/actions/workflows/common-js-ci.yml/badge.svg?branch=master)](https://github.com/configcat/common-js/actions/workflows/common-js-ci.yml) [![codecov](https://codecov.io/gh/configcat/common-js/branch/master/graph/badge.svg)](https://codecov.io/gh/configcat/common-js) [![Known Vulnerabilities](https://snyk.io/test/github/configcat/common-js/badge.svg?targetFile=package.json)](https://snyk.io/test/github/configcat/common-js?targetFile=package.json) [![Tree Shaking](https://badgen.net/bundlephobia/tree-shaking/configcat-common)](https://bundlephobia.com/result?p=configcat-common) ![License](https://img.shields.io/github/license/configcat/common-js.svg) \ 4 | [![NPM](https://nodei.co/npm/configcat-common.png)](https://nodei.co/npm/configcat-common/) 5 | 6 | ConfigCat Common library for JavaScript is a shared package that provides the common ConfigCat SDK logic for [ConfigCat SDK for Node.js](https://github.com/configcat/node-sdk) and [ConfigCat SDK for JavaScript](https://github.com/configcat/js-sdk). 7 | 8 | ConfigCat is a feature flag, feature toggle, and configuration management service that lets you launch new features and change your software configuration remotely without actually (re)deploying code. ConfigCat even helps you do controlled roll-outs like canary releases and blue-green deployments. 9 | 10 | ConfigCat is a [hosted feature flag service](https://configcat.com). Manage feature toggles across frontend, backend, mobile, desktop apps. [Alternative to LaunchDarkly](https://configcat.com). Management app + feature flag SDKs. 11 | 12 | ## Installing 13 | ``` 14 | npm install 15 | ``` 16 | 17 | ## Running the tests 18 | ``` 19 | npm test 20 | ``` 21 | 22 | ## Need help? 23 | https://configcat.com/support 24 | 25 | ## Contributing 26 | Contributions are welcome. For more info please read the [Contribution Guideline](CONTRIBUTING.md). 27 | 28 | ## About ConfigCat 29 | - [Documentation](https://configcat.com/docs) 30 | - [Blog](https://configcat.com/blog) 31 | 32 | # Troubleshooting 33 | ### Make sure you have the proper Node.js version installed 34 | You might run into errors caused by the wrong version of Node.js. To make sure you are using the recommended Node.js version follow these steps. 35 | 36 | 1. Have nvm (Node Version Manager - https://github.com/nvm-sh/nvm ) installed: 37 | 1. Run `nvm install`. This will install the compatible version of Node.js. 38 | 1. Run `nvm use`. This will use the compatible version of Node.js. -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #Run this script to update configcat-common to the latest and release a new version of configcat-js 2 | set -e #Making sure script stops on error 3 | npm test 4 | git push origin $(npm version patch) 5 | git push -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "configcat-common", 3 | "version": "9.4.0", 4 | "description": "ConfigCat is a configuration as a service that lets you manage your features and configurations without actually deploying new code.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "module": "lib/esm/index.js", 8 | "scripts": { 9 | "coverage": "cross-env nyc npm run test", 10 | "build": "tsc -p tsconfig.build.cjs.json && tsc -p tsconfig.build.esm.json", 11 | "prepare": "npm run build", 12 | "test": "cross-env TS_NODE_PROJECT=./tsconfig.mocha.json node --expose-gc node_modules/mocha/bin/_mocha --require ts-node/register 'test/**/*.ts' --exit --timeout 30000", 13 | "test-debug": "cross-env TS_NODE_PROJECT=./tsconfig.mocha.json node --expose-gc node_modules/mocha/bin/_mocha --fgrep \"TITLE_OF_TEST\" --require ts-node/register 'test/**/*.ts' --exit --timeout 30000", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix" 16 | }, 17 | "keywords": [ 18 | "configcat", 19 | "config", 20 | "configuration", 21 | "remote configuration", 22 | "configcat client", 23 | "feature flags", 24 | "feature toggle", 25 | "feature switch", 26 | "canary release", 27 | "soft launch", 28 | "release strategy" 29 | ], 30 | "author": "ConfigCat", 31 | "license": "MIT", 32 | "homepage": "https://configcat.com", 33 | "dependencies": { 34 | "tslib": "^2.4.1" 35 | }, 36 | "devDependencies": { 37 | "@types/chai": "4.3.4", 38 | "@types/mocha": "^10.0.1", 39 | "@types/node": "^18.11.18", 40 | "@types/tunnel": "^0.0.5", 41 | "@typescript-eslint/eslint-plugin": "^5.53.0", 42 | "@typescript-eslint/parser": "^5.53.0", 43 | "chai": "^4.3.7", 44 | "cross-env": "^7.0.3", 45 | "eslint": "^8.34.0", 46 | "eslint-plugin-import": "^2.27.5", 47 | "mocha": "^10.2.0", 48 | "moq.ts": "^7.4.1", 49 | "nyc": "^15.0.0", 50 | "source-map-support": "^0.5.21", 51 | "ts-node": "^10.9.1", 52 | "tunnel": "^0.0.6", 53 | "typescript": "^4.0.2" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/configcat/common-js" 58 | }, 59 | "nyc": { 60 | "extension": [ 61 | ".ts" 62 | ], 63 | "reporter": [ 64 | "text-summary", 65 | "json", 66 | "lcov" 67 | ], 68 | "all": true, 69 | "include": [ 70 | "src" 71 | ], 72 | "exclude": [ 73 | "src/Hash.ts", 74 | "src/Semver.ts" 75 | ] 76 | }, 77 | "sideEffects": false 78 | } 79 | -------------------------------------------------------------------------------- /samples/deno-sandbox/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Program", 9 | "type": "node", 10 | "request": "launch", 11 | "cwd": "${workspaceFolder}", 12 | "runtimeExecutable": "deno", 13 | "runtimeArgs": ["run", "--inspect-brk", "--allow-all", "main.ts"], 14 | "attachSimplePort": 9229, 15 | "outputCapture": "std" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /samples/deno-sandbox/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.importMap": "./import_map.json", 4 | "deno.unstable": true, 5 | "editor.defaultFormatter": "denoland.vscode-deno" 6 | } 7 | -------------------------------------------------------------------------------- /samples/deno-sandbox/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": "import_map.json" 3 | } 4 | -------------------------------------------------------------------------------- /samples/deno-sandbox/fake-config-fetcher.ts: -------------------------------------------------------------------------------- 1 | import type { IConfigFetcher, OptionsBase } from "src/index.ts"; 2 | import type { IFetchResponse } from "../../src/ConfigFetcher.ts"; 3 | 4 | export class FakeConfigFetcher implements IConfigFetcher { 5 | private currentFetchResponse: IFetchResponse = { statusCode: 404, reasonPhrase: "Not Found" }; 6 | private currentETag = 0; 7 | 8 | fetchLogic(_options: OptionsBase, lastEtag: string | null): Promise { 9 | return Promise.resolve(this.currentFetchResponse.statusCode === 200 && (this.currentETag + "") === lastEtag 10 | ? { statusCode: 304, reasonPhrase: "Not Modified" } 11 | : this.currentFetchResponse); 12 | } 13 | 14 | setSuccess(configJson: string): void { 15 | this.currentFetchResponse = { statusCode: 200, reasonPhrase: "OK", eTag: (++this.currentETag) + "", body: configJson }; 16 | } 17 | 18 | setError(statusCode: number, reasonPhrase: string): void { 19 | this.currentFetchResponse = { statusCode, reasonPhrase }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/deno-sandbox/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "std/": "https://deno.land/std/", 4 | "3rdparty/": "https://deno.land/x/", 5 | "src/": "../../src/", 6 | "../../src/index": "../../src/index.ts", 7 | "../../src/AutoPollConfigService": "../../src/AutoPollConfigService.ts", 8 | "../../src/ConfigCatCache": "../../src/ConfigCatCache.ts", 9 | "../../src/ConfigCatClient": "../../src/ConfigCatClient.ts", 10 | "../../src/ConfigCatClientOptions": "../../src/ConfigCatClientOptions.ts", 11 | "../../src/ConfigCatLogger": "../../src/ConfigCatLogger.ts", 12 | "../../src/ConfigFetcher": "../../src/ConfigFetcher.ts", 13 | "../../src/ConfigJson": "../../src/ConfigJson.ts", 14 | "../../src/ConfigServiceBase": "../../src/ConfigServiceBase.ts", 15 | "../../src/DefaultEventEmitter": "../../src/DefaultEventEmitter.ts", 16 | "../../src/EvaluateLogBuilder": "../../src/EvaluateLogBuilder.ts", 17 | "../../src/EventEmitter": "../../src/EventEmitter.ts", 18 | "../../src/FlagOverrides": "../../src/FlagOverrides.ts", 19 | "../../src/Hash": "../../src/Hash.ts", 20 | "../../src/Hooks": "../../src/Hooks.ts", 21 | "../../src/LazyLoadConfigService": "../../src/LazyLoadConfigService.ts", 22 | "../../src/ManualPollConfigService": "../../src/ManualPollConfigService.ts", 23 | "../../src/Polyfills": "../../src/Polyfills.ts", 24 | "../../src/ProjectConfig": "../../src/ProjectConfig.ts", 25 | "../../src/RolloutEvaluator": "../../src/RolloutEvaluator.ts", 26 | "../../src/Semver": "../../src/Semver.ts", 27 | "../../src/User": "../../src/User.ts", 28 | "../../src/Utils": "../../src/Utils.ts" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/deno-sandbox/main.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, PollingMode, User, createConsoleLogger, getClient } from "src/index.ts"; 2 | import * as path from "std/path/mod.ts"; 3 | import { FakeConfigFetcher } from "./fake-config-fetcher.ts"; 4 | 5 | const basePath = path.dirname(path.fromFileUrl(Deno.mainModule)); 6 | const sampleJsonPath = path.resolve(basePath, "sample.json"); 7 | const sampleJson = await Deno.readTextFile(sampleJsonPath); 8 | 9 | const configFetcher = new FakeConfigFetcher(); 10 | configFetcher.setSuccess(sampleJson); 11 | 12 | // Creating the ConfigCat client instance using the SDK Key 13 | const client = getClient( 14 | "PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ", 15 | PollingMode.AutoPoll, 16 | { 17 | // Setting log level to Info to show detailed feature flag evaluation 18 | logger: createConsoleLogger(LogLevel.Info), 19 | setupHooks: hooks => hooks 20 | .on("clientReady", () => console.log("Client is ready!")) 21 | }, 22 | { 23 | configFetcher, 24 | sdkType: "ConfigCat-Deno", 25 | sdkVersion: "0.0.0-sample" 26 | }); 27 | 28 | try { 29 | // Creating a user object to identify the user (optional) 30 | const user = new User(""); 31 | user.country = "US"; 32 | user.email = "configcat@example.com"; 33 | user.custom = { 34 | "subscriptionType": "Pro", 35 | "role": "Admin", 36 | "version": "1.0.0" 37 | }; 38 | 39 | // Accessing feature flag or setting value 40 | const value = await client.getValueAsync("isPOCFeatureEnabled", false, user); 41 | console.log(`isPOCFeatureEnabled: ${value}`); 42 | } 43 | finally { 44 | client.dispose(); 45 | } 46 | 47 | Deno.exit(); 48 | -------------------------------------------------------------------------------- /samples/deno-sandbox/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0, 5 | "s": "unEpMhsXD/Zv9MH0gkBqS0Hr97LCGUKpHHOhEfzTcY4=" 6 | }, 7 | "f": { 8 | "isAwesomeFeatureEnabled": { 9 | "t": 0, 10 | "r": [ 11 | { 12 | "c": [ 13 | { 14 | "u": { 15 | "a": "Identifier", 16 | "c": 0, 17 | "l": [ 18 | "gdf" 19 | ] 20 | } 21 | } 22 | ], 23 | "s": { 24 | "v": { 25 | "b": false 26 | }, 27 | "i": "2bdcee75" 28 | } 29 | }, 30 | { 31 | "c": [ 32 | { 33 | "u": { 34 | "a": "Identifier", 35 | "c": 0, 36 | "l": [ 37 | "dfgdf" 38 | ] 39 | } 40 | } 41 | ], 42 | "s": { 43 | "v": { 44 | "b": false 45 | }, 46 | "i": "2bdcee75" 47 | } 48 | } 49 | ], 50 | "p": [ 51 | { 52 | "p": 0, 53 | "v": { 54 | "b": true 55 | }, 56 | "i": "ca36009d" 57 | }, 58 | { 59 | "p": 100, 60 | "v": { 61 | "b": false 62 | }, 63 | "i": "2bdcee75" 64 | } 65 | ], 66 | "v": { 67 | "b": true 68 | }, 69 | "i": "ca36009d" 70 | }, 71 | "isPOCFeatureEnabled": { 72 | "t": 0, 73 | "r": [ 74 | { 75 | "c": [ 76 | { 77 | "u": { 78 | "a": "Email", 79 | "c": 2, 80 | "l": [ 81 | "@something.com" 82 | ] 83 | } 84 | } 85 | ], 86 | "s": { 87 | "v": { 88 | "b": false 89 | }, 90 | "i": "430bded3" 91 | } 92 | }, 93 | { 94 | "c": [ 95 | { 96 | "u": { 97 | "a": "Email", 98 | "c": 2, 99 | "l": [ 100 | "@example.com" 101 | ] 102 | } 103 | } 104 | ], 105 | "s": { 106 | "v": { 107 | "b": true 108 | }, 109 | "i": "9f21c24c" 110 | } 111 | } 112 | ], 113 | "v": { 114 | "b": false 115 | }, 116 | "i": "430bded3" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/ConfigCatCache.ts: -------------------------------------------------------------------------------- 1 | import type { LoggerWrapper } from "./ConfigCatLogger"; 2 | import { ProjectConfig } from "./ProjectConfig"; 3 | import { isPromiseLike } from "./Utils"; 4 | 5 | /** Defines the interface used by the ConfigCat SDK to store and retrieve downloaded config data. */ 6 | export interface IConfigCatCache { 7 | /** 8 | * Stores a data item into the cache. 9 | * @param key A string identifying the data item. 10 | * @param value The data item to cache. 11 | */ 12 | set(key: string, value: string): Promise | void; 13 | 14 | /** 15 | * Retrieves a data item from the cache. 16 | * @param key A string identifying the value. 17 | * @returns The cached data item or `null` or `undefined` if there is none. 18 | */ 19 | get(key: string): Promise | string | null | undefined; 20 | } 21 | 22 | /** @remarks Unchanged config is returned as is, changed config is wrapped in an array so we can distinguish between the two cases. */ 23 | export type CacheSyncResult = ProjectConfig | [changedConfig: ProjectConfig]; 24 | 25 | export interface IConfigCache { 26 | set(key: string, config: ProjectConfig): Promise | void; 27 | 28 | get(key: string): Promise | CacheSyncResult; 29 | 30 | getInMemory(): ProjectConfig; 31 | } 32 | 33 | export class InMemoryConfigCache implements IConfigCache { 34 | private cachedConfig: ProjectConfig = ProjectConfig.empty; 35 | 36 | set(_key: string, config: ProjectConfig): void { 37 | this.cachedConfig = config; 38 | } 39 | 40 | get(_key: string): ProjectConfig { 41 | return this.cachedConfig; 42 | } 43 | 44 | getInMemory(): ProjectConfig { 45 | return this.cachedConfig; 46 | } 47 | } 48 | 49 | export class ExternalConfigCache implements IConfigCache { 50 | private cachedConfig: ProjectConfig = ProjectConfig.empty; 51 | private cachedSerializedConfig: string | undefined; 52 | 53 | constructor( 54 | private readonly cache: IConfigCatCache, 55 | private readonly logger: LoggerWrapper) { 56 | } 57 | 58 | async set(key: string, config: ProjectConfig): Promise { 59 | try { 60 | if (!config.isEmpty) { 61 | this.cachedSerializedConfig = ProjectConfig.serialize(config); 62 | this.cachedConfig = config; 63 | } 64 | else { 65 | // We may have empty entries with timestamp > 0 (see the flooding prevention logic in ConfigServiceBase.fetchAsync). 66 | // In such cases we want to preserve the timestamp locally but don't want to store those entries into the external cache. 67 | this.cachedSerializedConfig = void 0; 68 | this.cachedConfig = config; 69 | return; 70 | } 71 | 72 | await this.cache.set(key, this.cachedSerializedConfig); 73 | } 74 | catch (err) { 75 | this.logger.configServiceCacheWriteError(err); 76 | } 77 | } 78 | 79 | private updateCachedConfig(externalSerializedConfig: string | null | undefined): CacheSyncResult { 80 | if (externalSerializedConfig == null || externalSerializedConfig === this.cachedSerializedConfig) { 81 | return this.cachedConfig; 82 | } 83 | 84 | const externalConfig = ProjectConfig.deserialize(externalSerializedConfig); 85 | const hasChanged = !ProjectConfig.contentEquals(externalConfig, this.cachedConfig); 86 | this.cachedConfig = externalConfig; 87 | this.cachedSerializedConfig = externalSerializedConfig; 88 | return hasChanged ? [this.cachedConfig] : this.cachedConfig; 89 | } 90 | 91 | get(key: string): Promise | CacheSyncResult { 92 | let cacheSyncResult: CacheSyncResult; 93 | 94 | try { 95 | const cacheGetResult = this.cache.get(key); 96 | 97 | // Take the async path only when the IConfigCatCache.get operation is asynchronous. 98 | if (isPromiseLike(cacheGetResult)) { 99 | return (async (cacheGetPromise) => { 100 | let cacheSyncResult: CacheSyncResult; 101 | 102 | try { 103 | cacheSyncResult = this.updateCachedConfig(await cacheGetPromise); 104 | } 105 | catch (err) { 106 | cacheSyncResult = this.cachedConfig; 107 | this.logger.configServiceCacheReadError(err); 108 | } 109 | 110 | return cacheSyncResult; 111 | })(cacheGetResult); 112 | } 113 | 114 | // Otherwise, keep the code flow synchronous so the config services can sync up 115 | // with the cache in their ctors synchronously (see ConfigServiceBase.syncUpWithCache). 116 | cacheSyncResult = this.updateCachedConfig(cacheGetResult); 117 | } 118 | catch (err) { 119 | cacheSyncResult = this.cachedConfig; 120 | this.logger.configServiceCacheReadError(err); 121 | } 122 | 123 | return cacheSyncResult; 124 | } 125 | 126 | getInMemory(): ProjectConfig { 127 | return this.cachedConfig; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/ConfigFetcher.ts: -------------------------------------------------------------------------------- 1 | import type { OptionsBase } from "./ConfigCatClientOptions"; 2 | import type { ProjectConfig } from "./ProjectConfig"; 3 | 4 | export enum FetchStatus { 5 | Fetched = 0, 6 | NotModified = 1, 7 | Errored = 2, 8 | } 9 | 10 | export class FetchResult { 11 | private constructor( 12 | public status: FetchStatus, 13 | public config: ProjectConfig, 14 | public errorMessage?: string, 15 | public errorException?: any) { 16 | } 17 | 18 | static success(config: ProjectConfig): FetchResult { 19 | return new FetchResult(FetchStatus.Fetched, config); 20 | } 21 | 22 | static notModified(config: ProjectConfig): FetchResult { 23 | return new FetchResult(FetchStatus.NotModified, config); 24 | } 25 | 26 | static error(config: ProjectConfig, errorMessage?: string, errorException?: any): FetchResult { 27 | return new FetchResult(FetchStatus.Errored, config, errorMessage ?? "Unknown error.", errorException); 28 | } 29 | } 30 | 31 | export interface IFetchResponse { 32 | statusCode: number; 33 | reasonPhrase: string; 34 | eTag?: string; 35 | body?: string; 36 | } 37 | 38 | export type FetchErrorCauses = { 39 | abort: []; 40 | timeout: [timeoutMs: number]; 41 | failure: [err?: any]; 42 | }; 43 | 44 | export class FetchError extends Error { 45 | readonly name = FetchError.name; 46 | args: FetchErrorCauses[TCause]; 47 | 48 | constructor(public cause: TCause, ...args: FetchErrorCauses[TCause]) { 49 | super(((cause: TCause, args: FetchErrorCauses[TCause]): string | undefined => { 50 | switch (cause) { 51 | case "abort": 52 | return "Request was aborted."; 53 | case "timeout": 54 | const [timeoutMs] = args as FetchErrorCauses["timeout"]; 55 | return `Request timed out. Timeout value: ${timeoutMs}ms`; 56 | case "failure": 57 | const [err] = args as FetchErrorCauses["failure"]; 58 | const message = "Request failed due to a network or protocol error."; 59 | return err 60 | ? message + " " + (err instanceof Error ? err.message : err + "") 61 | : message; 62 | } 63 | })(cause, args)); 64 | 65 | // NOTE: due to a known issue in the TS compiler, instanceof is broken when subclassing Error and targeting ES5 or earlier 66 | // (see https://github.com/microsoft/TypeScript/issues/13965). 67 | // Thus, we need to manually fix the prototype chain as recommended in the TS docs 68 | // (see https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work) 69 | if (!(this instanceof FetchError)) { 70 | (Object.setPrototypeOf || ((o, proto) => o["__proto__"] = proto))(this, FetchError.prototype); 71 | } 72 | this.args = args; 73 | } 74 | } 75 | 76 | export interface IConfigFetcher { 77 | fetchLogic(options: OptionsBase, lastEtag: string | null): Promise; 78 | } 79 | -------------------------------------------------------------------------------- /src/DefaultEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import type { IEventEmitter } from "./EventEmitter"; 2 | 3 | type Listener = { fn: (...args: any[]) => void; once?: boolean }; 4 | type Listeners = Listener | Listener[] & { fn?: never }; 5 | 6 | function isSingle(listeners: Listeners): listeners is Listener { 7 | return !!listeners.fn; 8 | } 9 | 10 | // NOTE: It's better to place this class into a separate module so 11 | // it can be omitted from the final bundle in case we choose to 12 | // make the common library EventEmitter implementation-agnostic in the future. 13 | 14 | /** A platform-independent implementation of `IEventEmitter`. */ 15 | export class DefaultEventEmitter implements IEventEmitter { 16 | private events: Record = {}; 17 | private eventCount = 0; 18 | 19 | private addListenerCore(eventName: string | symbol, fn: (...args: any[]) => void, once: boolean) { 20 | if (typeof fn !== "function") { 21 | throw new TypeError("Listener must be a function"); 22 | } 23 | 24 | // TODO: remove `as string` when updating to TypeScript 4.4 or newer (https://stackoverflow.com/a/64943542/8656352) 25 | const listeners = this.events[eventName as string]; 26 | const listener: Listener = { fn, once }; 27 | 28 | if (!listeners) { 29 | this.events[eventName as string] = listener; 30 | this.eventCount++; 31 | } 32 | else if (isSingle(listeners)) { 33 | this.events[eventName as string] = [listeners, listener]; 34 | } 35 | else { 36 | listeners.push(listener); 37 | } 38 | 39 | return this; 40 | } 41 | 42 | private removeListenerCore(eventName: string | symbol, state: TState, isMatch: (listener: Listener, state: TState) => boolean) { 43 | const listeners = this.events[eventName as string]; 44 | 45 | if (!listeners) { 46 | return this; 47 | } 48 | 49 | if (!isSingle(listeners)) { 50 | for (let i = listeners.length - 1; i >= 0; i--) { 51 | if (isMatch(listeners[i], state)) { 52 | listeners.splice(i, 1); 53 | if (!listeners.length) { 54 | this.removeEvent(eventName); 55 | } 56 | else if (listeners.length === 1) { 57 | this.events[eventName as string] = listeners[0]; 58 | } 59 | break; 60 | } 61 | } 62 | } 63 | else if (isMatch(listeners, state)) { 64 | this.removeEvent(eventName); 65 | } 66 | 67 | return this; 68 | } 69 | 70 | private removeEvent(eventName: string | symbol) { 71 | if (--this.eventCount === 0) { 72 | this.events = {}; 73 | } 74 | else { 75 | delete this.events[eventName as string]; 76 | } 77 | } 78 | 79 | addListener: (eventName: string | symbol, listener: (...args: any[]) => void) => this = this.on; 80 | 81 | on(eventName: string | symbol, listener: (...args: any[]) => void): this { 82 | return this.addListenerCore(eventName, listener, false); 83 | } 84 | 85 | once(eventName: string | symbol, listener: (...args: any[]) => void): this { 86 | return this.addListenerCore(eventName, listener, true); 87 | } 88 | 89 | removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this { 90 | if (typeof listener !== "function") { 91 | throw new TypeError("Listener must be a function"); 92 | } 93 | 94 | return this.removeListenerCore(eventName, listener, (listener, fn) => listener.fn === fn); 95 | } 96 | 97 | off: (eventName: string | symbol, listener: (...args: any[]) => void) => this = this.removeListener; 98 | 99 | removeAllListeners(eventName?: string | symbol | undefined): this { 100 | if (!eventName) { 101 | this.events = {}; 102 | this.eventCount = 0; 103 | } 104 | else if (this.events[eventName as string]) { 105 | this.removeEvent(eventName); 106 | } 107 | 108 | return this; 109 | } 110 | 111 | listeners(eventName: string | symbol): Function[] { 112 | const listeners = this.events[eventName as string]; 113 | 114 | if (!listeners) { 115 | return []; 116 | } 117 | 118 | if (isSingle(listeners)) { 119 | return [listeners.fn]; 120 | } 121 | 122 | const length = listeners.length, fns = new Array(length); 123 | for (let i = 0; i < length; i++) { 124 | fns[i] = listeners[i].fn; 125 | } 126 | return fns; 127 | } 128 | 129 | listenerCount(eventName: string | symbol): number { 130 | const listeners = this.events[eventName as string]; 131 | 132 | if (!listeners) { 133 | return 0; 134 | } 135 | 136 | if (isSingle(listeners)) { 137 | return 1; 138 | } 139 | 140 | return listeners.length; 141 | } 142 | 143 | eventNames(): (string | symbol)[] { 144 | const names: (string | symbol)[] = []; 145 | 146 | if (this.eventCount === 0) { 147 | return names; 148 | } 149 | 150 | const events = this.events; 151 | for (const name in events) { 152 | if (Object.prototype.hasOwnProperty.call(events, name)) { 153 | names.push(name); 154 | } 155 | } 156 | 157 | if (Object.getOwnPropertySymbols) { 158 | return names.concat(Object.getOwnPropertySymbols(events)); 159 | } 160 | 161 | return names; 162 | } 163 | 164 | emit(eventName: string | symbol, arg0?: any, arg1?: any, arg2?: any, arg3?: any, ...moreArgs: any[]): boolean { 165 | let listeners = this.events[eventName as string]; 166 | 167 | if (!listeners) { 168 | return false; 169 | } 170 | 171 | let listener: Listener, length: number; 172 | 173 | if (isSingle(listeners)) { 174 | [listener, length] = [listeners, 1]; 175 | } 176 | else { 177 | // According to the specification, potential removes during emit should not change the list of notified listeners, 178 | // so we need to create a local copy of the current listeners. 179 | listeners = listeners.slice(); 180 | [listener, length] = [listeners[0], listeners.length]; 181 | } 182 | 183 | const argCount = arguments.length - 1; 184 | 185 | for (let i = 0; ;) { 186 | if (listener.once) { 187 | this.removeListenerCore(eventName, listener, (listener, toRemove) => listener === toRemove); 188 | } 189 | 190 | switch (argCount) { 191 | case 0: listener.fn.call(this); break; 192 | case 1: listener.fn.call(this, arg0); break; 193 | case 2: listener.fn.call(this, arg0, arg1); break; 194 | case 3: listener.fn.call(this, arg0, arg1, arg2); break; 195 | case 4: listener.fn.call(this, arg0, arg1, arg2, arg3); break; 196 | default: 197 | const args = new Array(argCount); 198 | for (let j = 0; j < argCount; j++) { 199 | // eslint-disable-next-line prefer-rest-params 200 | args[j] = arguments[j + 1]; 201 | } 202 | listener.fn.apply(this, args); 203 | break; 204 | } 205 | 206 | if (++i >= length) { 207 | break; 208 | } 209 | 210 | listener = (listeners as Listener[])[i]; 211 | } 212 | 213 | return true; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | // The interfaces below define a subset of Node's EventEmitter (see https://nodejs.org/api/events.html#class-eventemitter), 2 | // so they are (structurally) compatible with instances of EventEmitter. 3 | 4 | type Events = Record; 5 | 6 | /** Defines methods for subscribing to/unsubscribing from events. */ 7 | export interface IEventProvider { 8 | /** 9 | * Alias for `emitter.on(eventName, listener)`. 10 | */ 11 | addListener(eventName: TEventName, listener: (...args: TEvents[TEventName]) => void): this; 12 | 13 | /** 14 | * Adds the `listener` function to the end of the listeners array for the 15 | * event named `eventName`. No checks are made to see if the `listener` has 16 | * already been added. Multiple calls passing the same combination of `eventName` and 17 | * `listener` will result in the `listener` being added, and called, multiple times. 18 | * 19 | * Returns a reference to the `EventEmitter`, so that calls can be chained. 20 | * 21 | * @param eventName The name of the event. 22 | * @param listener The callback function 23 | */ 24 | on(eventName: TEventName, listener: (...args: TEvents[TEventName]) => void): this; 25 | 26 | /** 27 | * Adds a **one-time** `listener` function for the event named `eventName`. The 28 | * next time `eventName` is triggered, this listener is removed and then invoked. 29 | * 30 | * Returns a reference to the `EventEmitter`, so that calls can be chained. 31 | * 32 | * @param eventName The name of the event. 33 | * @param listener The callback function 34 | */ 35 | once(eventName: TEventName, listener: (...args: TEvents[TEventName]) => void): this; 36 | 37 | /** 38 | * Removes the specified `listener` from the listener array for the event named `eventName`. 39 | * 40 | * `removeListener()` will remove, at most, one instance of a listener from the 41 | * listener array. If any single listener has been added multiple times to the 42 | * listener array for the specified `eventName`, then `removeListener()` must be 43 | * called multiple times to remove each instance. 44 | * 45 | * Once an event is emitted, all listeners attached to it at the 46 | * time of emitting are called in order. This implies that any `removeListener()` or `removeAllListeners()` 47 | * calls _after_ emitting and _before_ the last listener finishes execution will 48 | * not remove them from `emit()` in progress. Subsequent events behave as expected. 49 | * 50 | * Because listeners are managed using an internal array, calling this will 51 | * change the position indices of any listener registered _after_ the listener 52 | * being removed. This will not impact the order in which listeners are called, 53 | * but it means that any copies of the listener array as returned by 54 | * the `emitter.listeners()` method will need to be recreated. 55 | * 56 | * When a single function has been added as a handler multiple times for a single 57 | * event (as in the example below), `removeListener()` will remove the most 58 | * recently added instance. 59 | * 60 | * Returns a reference to the `EventEmitter`, so that calls can be chained. 61 | */ 62 | removeListener(eventName: TEventName, listener: (...args: TEvents[TEventName]) => void): this; 63 | 64 | /** 65 | * Alias for `emitter.removeListener()`. 66 | */ 67 | off(eventName: TEventName, listener: (...args: TEvents[TEventName]) => void): this; 68 | 69 | /** 70 | * Removes all listeners, or those of the specified `eventName`. 71 | * 72 | * It is bad practice to remove listeners added elsewhere in the code, 73 | * particularly when the `EventEmitter` instance was created by some other 74 | * component or module (e.g. sockets or file streams). 75 | * 76 | * Returns a reference to the `EventEmitter`, so that calls can be chained. 77 | */ 78 | removeAllListeners(eventName?: keyof TEvents): this; 79 | 80 | /** 81 | * Returns a copy of the array of listeners for the event named `eventName`. 82 | */ 83 | listeners(eventName: keyof TEvents): Function[]; 84 | 85 | /** 86 | * Returns the number of listeners listening to the event named `eventName`. 87 | * @param eventName The name of the event being listened for 88 | */ 89 | listenerCount(eventName: keyof TEvents): number; 90 | 91 | /** 92 | * Returns an array listing the events for which the emitter has registered 93 | * listeners. The values in the array are strings or `Symbol`s. 94 | */ 95 | eventNames(): Array; 96 | } 97 | 98 | /** Defines methods for emitting events. */ 99 | export interface IEventEmitter extends IEventProvider { 100 | /** 101 | * Synchronously calls each of the listeners registered for the event named `eventName`, 102 | * in the order they were registered, passing the supplied arguments to each. 103 | * 104 | * Returns `true` if the event had listeners, `false` otherwise. 105 | */ 106 | emit(eventName: TEventName, ...args: TEvents[TEventName]): boolean; 107 | } 108 | 109 | export class NullEventEmitter implements IEventEmitter { 110 | addListener: () => this = this.on; 111 | 112 | on(): this { return this; } 113 | 114 | once(): this { return this; } 115 | 116 | removeListener(): this { return this; } 117 | 118 | off: () => this = this.removeListener; 119 | 120 | removeAllListeners(): this { return this; } 121 | 122 | listeners(): Function[] { return []; } 123 | 124 | listenerCount(): number { return 0; } 125 | 126 | eventNames(): (string | symbol)[] { return []; } 127 | 128 | emit(): boolean { return false; } 129 | } 130 | -------------------------------------------------------------------------------- /src/FlagOverrides.ts: -------------------------------------------------------------------------------- 1 | import type { SettingValue } from "./ProjectConfig"; 2 | import { Setting } from "./ProjectConfig"; 3 | 4 | /** 5 | * Specifies the behaviours for flag overrides. 6 | */ 7 | export enum OverrideBehaviour { 8 | /** 9 | * When evaluating values, the SDK will not use feature flags and settings from the ConfigCat CDN, but it will use 10 | * all feature flags and settings that are loaded from local-override sources. 11 | */ 12 | LocalOnly = 0, 13 | /** 14 | * When evaluating values, the SDK will use all feature flags and settings that are downloaded from the ConfigCat CDN, 15 | * plus all feature flags and settings that are loaded from local-override sources. If a feature flag or a setting is 16 | * defined both in the fetched and the local-override source then the local-override version will take precedence. 17 | */ 18 | LocalOverRemote = 1, 19 | /** 20 | * When evaluating values, the SDK will use all feature flags and settings that are downloaded from the ConfigCat CDN, 21 | * plus all feature flags and settings that are loaded from local-override sources. If a feature flag or a setting is 22 | * defined both in the fetched and the local-override source then the fetched version will take precedence. 23 | */ 24 | RemoteOverLocal = 2, 25 | } 26 | 27 | export interface IOverrideDataSource { 28 | getOverrides(): Promise<{ [name: string]: Setting }>; 29 | 30 | getOverridesSync(): { [name: string]: Setting }; 31 | } 32 | 33 | export class MapOverrideDataSource implements IOverrideDataSource { 34 | private static getCurrentSettings(map: { [name: string]: NonNullable }) { 35 | return Object.fromEntries(Object.entries(map) 36 | .map(([key, value]) => [key, Setting.fromValue(value)])); 37 | } 38 | 39 | private readonly initialSettings: { [name: string]: Setting }; 40 | private readonly map?: { [name: string]: NonNullable }; 41 | 42 | private readonly ["constructor"]!: typeof MapOverrideDataSource; 43 | 44 | constructor(map: { [name: string]: NonNullable }, watchChanges?: boolean) { 45 | this.initialSettings = this.constructor.getCurrentSettings(map); 46 | if (watchChanges) { 47 | this.map = map; 48 | } 49 | } 50 | 51 | getOverrides(): Promise<{ [name: string]: Setting }> { 52 | return Promise.resolve(this.getOverridesSync()); 53 | } 54 | 55 | getOverridesSync(): { [name: string]: Setting } { 56 | return this.map 57 | ? this.constructor.getCurrentSettings(this.map) 58 | : this.initialSettings; 59 | } 60 | } 61 | 62 | export class FlagOverrides { 63 | constructor( 64 | public dataSource: IOverrideDataSource, 65 | public behaviour: OverrideBehaviour) { 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Hash.ts: -------------------------------------------------------------------------------- 1 | import { utf8Encode } from "./Utils"; 2 | 3 | export function sha1(msg: string) { 4 | function rotate_left(n: number, s: number) { 5 | var t4 = ( n<>>(32-s)); 6 | return t4; 7 | }; 8 | var blockstart; 9 | var i, j; 10 | var W = new Array(80); 11 | var H0 = 0x67452301; 12 | var H1 = 0xEFCDAB89; 13 | var H2 = 0x98BADCFE; 14 | var H3 = 0x10325476; 15 | var H4 = 0xC3D2E1F0; 16 | var A, B, C, D, E; 17 | var temp; 18 | msg = utf8Encode(msg); 19 | var msg_len = msg.length; 20 | var word_array = new Array(); 21 | for( i=0; i>>29 ); 43 | word_array.push( (msg_len<<3)&0x0ffffffff ); 44 | for ( blockstart=0; blockstart>> amount) | (value << (32 - amount)); 97 | }; 98 | 99 | const lengthProperty = "length"; 100 | var mathPow = Math.pow; 101 | var maxWord = mathPow(2, 32); 102 | var i, j; // Used as a counter across the whole file 103 | 104 | var precomputedData = sha256 as { h?: number[], k?: number[] }; 105 | var hash = precomputedData.h!; 106 | var k = precomputedData.k; 107 | if (!k) { 108 | // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes 109 | // (we actually calculate the first 64, but extra values are just ignored) 110 | hash = []; 111 | // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes 112 | k = []; 113 | 114 | var isComposite: {[n: number]: number | undefined } = {}; 115 | for (var candidate = 2, primeCounter = 0; primeCounter < 64; candidate++) { 116 | if (!isComposite[candidate]) { 117 | for (i = 0; i < 313; i += candidate) { 118 | isComposite[i] = candidate; 119 | } 120 | hash[primeCounter] = (mathPow(candidate, .5) * maxWord) | 0; 121 | k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0; 122 | } 123 | } 124 | 125 | precomputedData.h = hash = hash.slice(0, 8); 126 | precomputedData.k = k; 127 | } 128 | 129 | var asciiBitLength = msgUtf8[lengthProperty] * 8; 130 | msgUtf8 += '\x80' // Append Ƈ' bit (plus zero padding) 131 | 132 | var words: number[] = []; 133 | while (msgUtf8[lengthProperty] % 64 - 56) msgUtf8 += '\x00' // More zero padding 134 | for (i = 0; i < msgUtf8[lengthProperty]; i++) { 135 | j = msgUtf8.charCodeAt(i); 136 | words[i >> 2] |= j << ((3 - i) % 4) * 8; 137 | } 138 | words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0); 139 | words[words[lengthProperty]] = (asciiBitLength) 140 | 141 | // process each chunk 142 | for (j = 0; j < words[lengthProperty];) { 143 | var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration 144 | var oldHash = hash; 145 | // This is now the undefinedworking hash", often labelled as variables a...g 146 | // (we have to truncate as well, otherwise extra entries at the end accumulate 147 | hash = hash.slice(0, 8); 148 | 149 | for (i = 0; i < 64; i++) { 150 | // Expand the message into 64 words 151 | // Used below if 152 | var w15 = w[i - 15], w2 = w[i - 2]; 153 | 154 | // Iterate 155 | var a = hash[0], e = hash[4]; 156 | var temp1 = hash[7] 157 | + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1 158 | + ((e & hash[5]) ^ ((~e) & hash[6])) // ch 159 | + k[i] 160 | // Expand the message schedule if needed 161 | + (w[i] = (i < 16) ? w[i] : ( 162 | w[i - 16] 163 | + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) // s0 164 | + w[i - 7] 165 | + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) // s1 166 | ) | 0 167 | ); 168 | // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble 169 | var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0 170 | + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); // maj 171 | 172 | hash = [(temp1 + temp2) | 0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice() 173 | hash[4] = (hash[4] + temp1) | 0; 174 | } 175 | 176 | for (i = 0; i < 8; i++) { 177 | hash[i] = (hash[i] + oldHash[i]) | 0; 178 | } 179 | } 180 | 181 | return toHexString(hash, 8); 182 | } 183 | 184 | function toHexString(int32Array: number[], count?: number) { 185 | const hexDigits = "0123456789abcdef"; 186 | var result = ""; 187 | count ??= int32Array.length; 188 | for (let i = 0; i < count; i++) { 189 | for (let j = 3; j >= 0; j--) { 190 | const b = (int32Array[i] >> (j << 3)) & 0xFF; 191 | result += hexDigits[b >> 4]; 192 | result += hexDigits[b & 0xF]; 193 | } 194 | } 195 | return result; 196 | } -------------------------------------------------------------------------------- /src/Hooks.ts: -------------------------------------------------------------------------------- 1 | import type { ClientCacheState } from "./ConfigServiceBase"; 2 | import type { IEventEmitter, IEventProvider } from "./EventEmitter"; 3 | import { NullEventEmitter } from "./EventEmitter"; 4 | import type { IConfig } from "./ProjectConfig"; 5 | import type { IEvaluationDetails } from "./RolloutEvaluator"; 6 | 7 | /** Hooks (events) that can be emitted by `ConfigCatClient`. */ 8 | export type HookEvents = { 9 | /** 10 | * Occurs when the client reaches the ready state, i.e. completes initialization. 11 | * 12 | * @remarks Ready state is reached as soon as the initial sync with the external cache (if any) completes. 13 | * If this does not produce up-to-date config data, and the client is online (i.e. HTTP requests are allowed), 14 | * the first config fetch operation is also awaited in Auto Polling mode before ready state is reported. 15 | * 16 | * That is, reaching the ready state usually means the client is ready to evaluate feature flags and settings. 17 | * However, please note that this is not guaranteed. In case of initialization failure or timeout, the internal cache 18 | * may be empty or expired even after the ready state is reported. You can verify this by checking the `cacheState` parameter. 19 | */ 20 | clientReady: [cacheState: ClientCacheState]; 21 | /** Occurs after the value of a feature flag of setting has been evaluated. */ 22 | flagEvaluated: [evaluationDetails: IEvaluationDetails]; 23 | /** 24 | * Occurs after the internally cached config has been updated to a newer version, either as a result of synchronization 25 | * with the external cache, or as a result of fetching a newer version from the ConfigCat CDN. 26 | */ 27 | configChanged: [newConfig: IConfig]; 28 | /** Occurs in the case of a failure in the client. */ 29 | clientError: [message: string, exception?: any]; 30 | }; 31 | 32 | /** Defines hooks (events) for providing notifications of `ConfigCatClient`'s actions. */ 33 | export interface IProvidesHooks extends IEventProvider { } 34 | 35 | const disconnectedEventEmitter = new NullEventEmitter(); 36 | 37 | export class Hooks implements IProvidesHooks, IEventEmitter { 38 | private eventEmitter: IEventEmitter; 39 | 40 | constructor(eventEmitter: IEventEmitter) { 41 | this.eventEmitter = eventEmitter; 42 | } 43 | 44 | tryDisconnect(): boolean { 45 | // Replacing the current IEventEmitter object (eventEmitter) with a special instance of IEventEmitter (disconnectedEventEmitter) achieves multiple things: 46 | // 1. determines whether the hooks instance has already been disconnected or not, 47 | // 2. removes implicit references to subscriber objects (so this instance won't keep them alive under any circumstances), 48 | // 3. makes sure that future subscriptions are ignored from this point on. 49 | const originalEventEmitter = this.eventEmitter as IEventEmitter; 50 | this.eventEmitter = disconnectedEventEmitter; 51 | 52 | return originalEventEmitter !== disconnectedEventEmitter; 53 | } 54 | 55 | /** @inheritdoc */ 56 | addListener: (eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void) => this = this.on; 57 | 58 | /** @inheritdoc */ 59 | on(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this { 60 | this.eventEmitter.on(eventName, listener as (...args: any[]) => void); 61 | return this; 62 | } 63 | 64 | /** @inheritdoc */ 65 | once(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this { 66 | this.eventEmitter.once(eventName, listener as (...args: any[]) => void); 67 | return this; 68 | } 69 | 70 | /** @inheritdoc */ 71 | removeListener(eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void): this { 72 | this.eventEmitter.removeListener(eventName, listener as (...args: any[]) => void); 73 | return this; 74 | } 75 | 76 | /** @inheritdoc */ 77 | off: (eventName: TEventName, listener: (...args: HookEvents[TEventName]) => void) => this = this.removeListener; 78 | 79 | /** @inheritdoc */ 80 | removeAllListeners(eventName?: keyof HookEvents): this { 81 | this.eventEmitter.removeAllListeners(eventName); 82 | return this; 83 | } 84 | 85 | /** @inheritdoc */ 86 | listeners(eventName: keyof HookEvents): Function[] { 87 | return this.eventEmitter.listeners(eventName); 88 | } 89 | 90 | /** @inheritdoc */ 91 | listenerCount(eventName: keyof HookEvents): number { 92 | return this.eventEmitter.listenerCount(eventName); 93 | } 94 | 95 | /** @inheritdoc */ 96 | eventNames(): Array { 97 | return this.eventEmitter.eventNames() as Array; 98 | } 99 | 100 | /** @inheritdoc */ 101 | emit(eventName: TEventName, ...args: HookEvents[TEventName]): boolean { 102 | return this.eventEmitter.emit(eventName, ...args); 103 | } 104 | } 105 | 106 | // Strong back-references to the client instance must be avoided so GC can collect it when user doesn't have references to it any more. 107 | // E.g. if a strong reference chain like AutoPollConfigService -> ... -> ConfigCatClient existed, the client instance could not be collected 108 | // because the background polling loop would keep the AutoPollConfigService alive indefinetely, which in turn would keep alive ConfigCatClient. 109 | // We need to break such strong reference chains with a weak reference somewhere. As consumers are free to add hook event handlers which 110 | // close over the client instance (e.g. `client.on("configChanged", cfg => { client.GetValue(...) }`), that is, a chain like 111 | // AutoPollConfigService -> Hooks -> event handler -> ConfigCatClient can be created, it is the hooks reference that we need to make weak. 112 | export type SafeHooksWrapper = { 113 | emit(eventName: TEventName, ...args: HookEvents[TEventName]): boolean; 114 | } 115 | -------------------------------------------------------------------------------- /src/LazyLoadConfigService.ts: -------------------------------------------------------------------------------- 1 | import type { LazyLoadOptions } from "./ConfigCatClientOptions"; 2 | import type { LoggerWrapper } from "./ConfigCatLogger"; 3 | import type { IConfigFetcher } from "./ConfigFetcher"; 4 | import type { IConfigService, RefreshResult } from "./ConfigServiceBase"; 5 | import { ClientCacheState, ConfigServiceBase } from "./ConfigServiceBase"; 6 | import type { ProjectConfig } from "./ProjectConfig"; 7 | 8 | export class LazyLoadConfigService extends ConfigServiceBase implements IConfigService { 9 | 10 | private readonly cacheTimeToLiveMs: number; 11 | readonly readyPromise: Promise; 12 | 13 | constructor(configFetcher: IConfigFetcher, options: LazyLoadOptions) { 14 | 15 | super(configFetcher, options); 16 | 17 | this.cacheTimeToLiveMs = options.cacheTimeToLiveSeconds * 1000; 18 | 19 | const initialCacheSyncUp = this.syncUpWithCache(); 20 | this.readyPromise = this.getReadyPromise(initialCacheSyncUp, async initialCacheSyncUp => this.getCacheState(await initialCacheSyncUp)); 21 | } 22 | 23 | async getConfig(): Promise { 24 | this.options.logger.debug("LazyLoadConfigService.getConfig() called."); 25 | 26 | function logExpired(logger: LoggerWrapper, appendix = "") { 27 | logger.debug(`LazyLoadConfigService.getConfig(): cache is empty or expired${appendix}.`); 28 | } 29 | 30 | let cachedConfig = await this.syncUpWithCache(); 31 | 32 | if (cachedConfig.isExpired(this.cacheTimeToLiveMs)) { 33 | if (!this.isOffline) { 34 | logExpired(this.options.logger, ", calling refreshConfigCoreAsync()"); 35 | [, cachedConfig] = await this.refreshConfigCoreAsync(cachedConfig); 36 | } 37 | else { 38 | logExpired(this.options.logger); 39 | } 40 | return cachedConfig; 41 | } 42 | 43 | this.options.logger.debug("LazyLoadConfigService.getConfig(): cache is valid, returning from cache."); 44 | return cachedConfig; 45 | } 46 | 47 | refreshConfigAsync(): Promise<[RefreshResult, ProjectConfig]> { 48 | this.options.logger.debug("LazyLoadConfigService.refreshConfigAsync() called."); 49 | return super.refreshConfigAsync(); 50 | } 51 | 52 | getCacheState(cachedConfig: ProjectConfig): ClientCacheState { 53 | if (cachedConfig.isEmpty) { 54 | return ClientCacheState.NoFlagData; 55 | } 56 | 57 | if (cachedConfig.isExpired(this.cacheTimeToLiveMs)) { 58 | return ClientCacheState.HasCachedFlagDataOnly; 59 | } 60 | 61 | return ClientCacheState.HasUpToDateFlagData; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ManualPollConfigService.ts: -------------------------------------------------------------------------------- 1 | import type { ManualPollOptions } from "./ConfigCatClientOptions"; 2 | import type { IConfigFetcher } from "./ConfigFetcher"; 3 | import type { IConfigService, RefreshResult } from "./ConfigServiceBase"; 4 | import { ClientCacheState, ConfigServiceBase } from "./ConfigServiceBase"; 5 | import type { ProjectConfig } from "./ProjectConfig"; 6 | 7 | export class ManualPollConfigService extends ConfigServiceBase implements IConfigService { 8 | 9 | readonly readyPromise: Promise; 10 | 11 | constructor(configFetcher: IConfigFetcher, options: ManualPollOptions) { 12 | 13 | super(configFetcher, options); 14 | 15 | const initialCacheSyncUp = this.syncUpWithCache(); 16 | this.readyPromise = this.getReadyPromise(initialCacheSyncUp, async initialCacheSyncUp => this.getCacheState(await initialCacheSyncUp)); 17 | } 18 | 19 | getCacheState(cachedConfig: ProjectConfig): ClientCacheState { 20 | if (cachedConfig.isEmpty) { 21 | return ClientCacheState.NoFlagData; 22 | } 23 | 24 | return ClientCacheState.HasCachedFlagDataOnly; 25 | } 26 | 27 | async getConfig(): Promise { 28 | this.options.logger.debug("ManualPollService.getConfig() called."); 29 | return await this.syncUpWithCache(); 30 | } 31 | 32 | refreshConfigAsync(): Promise<[RefreshResult, ProjectConfig]> { 33 | this.options.logger.debug("ManualPollService.refreshConfigAsync() called."); 34 | return super.refreshConfigAsync(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Polyfills.ts: -------------------------------------------------------------------------------- 1 | export function setupPolyfills(): void { 2 | // Object.values 3 | if (typeof Object.values === "undefined") { 4 | Object.values = ObjectValuesPolyfill; 5 | } 6 | 7 | // Object.entries 8 | if (typeof Object.entries === "undefined") { 9 | Object.entries = ObjectEntriesPolyfill; 10 | } 11 | 12 | // Object.fromEntries 13 | if (typeof Object.fromEntries === "undefined") { 14 | Object.fromEntries = ObjectFromEntriesPolyfill; 15 | } 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/naming-convention 19 | export function ObjectValuesPolyfill(o: { [s: string]: T } | ArrayLike): T[] { 20 | const result: T[] = []; 21 | for (const key of Object.keys(o)) { 22 | result.push((o as any)[key]); 23 | } 24 | return result; 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/naming-convention 28 | export function ObjectEntriesPolyfill(o: { [s: string]: T } | ArrayLike): [string, T][] { 29 | const result: [string, T][] = []; 30 | for (const key of Object.keys(o)) { 31 | result.push([key, (o as any)[key]]); 32 | } 33 | return result; 34 | } 35 | 36 | // eslint-disable-next-line @typescript-eslint/naming-convention 37 | export function ObjectFromEntriesPolyfill(entries: Iterable): { [k: string]: T } { 38 | // TODO: change `k: string` to `k: PropertyKey` in the following line and remove `as string` below 39 | // when updating to TypeScript 4.4 or newer (https://stackoverflow.com/a/64943542/8656352) 40 | const result: { [k: string]: T } = {}; 41 | if (Array.isArray(entries)) { 42 | for (const [key, value] of entries) { 43 | result[key] = value; 44 | } 45 | } 46 | else if (typeof Symbol !== "undefined" && entries?.[Symbol.iterator]) { 47 | const iterator = entries[Symbol.iterator](); 48 | let element: readonly [PropertyKey, T], done: boolean | undefined; 49 | while (({ value: element, done } = iterator.next(), !done)) { 50 | const [key, value] = element; 51 | result[key as string] = value; 52 | } 53 | } 54 | else { 55 | throw new TypeError("Object.fromEntries() requires a single iterable argument"); 56 | } 57 | return result; 58 | } 59 | 60 | export function getWeakRefStub(): WeakRefConstructor { 61 | type WeakRefImpl = WeakRef & { target: T }; 62 | 63 | // eslint-disable-next-line @typescript-eslint/naming-convention 64 | const WeakRef = function(this: WeakRefImpl, target: T) { 65 | this.target = target; 66 | } as Function as WeakRefConstructor & { isFallback: boolean }; 67 | 68 | WeakRef.prototype.deref = function(this: WeakRefImpl) { 69 | return this.target; 70 | }; 71 | 72 | WeakRef.isFallback = true; 73 | 74 | return WeakRef; 75 | } 76 | 77 | export const isWeakRefAvailable = (): boolean => typeof WeakRef === "function"; 78 | -------------------------------------------------------------------------------- /src/User.ts: -------------------------------------------------------------------------------- 1 | export type WellKnownUserObjectAttribute = "Identifier" | "Email" | "Country"; 2 | 3 | export type UserAttributeValue = string | number | Date | ReadonlyArray; 4 | 5 | /** 6 | * User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. 7 | * @remarks 8 | * Please note that the `User` class is not designed to be used as a DTO (data transfer object). 9 | * (Since the type of the `custom` property is polymorphic, it's not guaranteed that deserializing a serialized instance produces an instance with an identical or even valid data content.) 10 | **/ 11 | export class User { 12 | constructor( 13 | /** The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) */ 14 | public identifier: string, 15 | /** Email address of the user. */ 16 | public email?: string, 17 | /** Country of the user. */ 18 | public country?: string, 19 | /** 20 | * Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) 21 | * @remarks 22 | * All comparators support `string` values as User Object attribute (in some cases they need to be provided in a specific format though, see below), 23 | * but some of them also support other types of values. It depends on the comparator how the values will be handled. The following rules apply: 24 | * 25 | * **Text-based comparators** (EQUALS, IS ONE OF, etc.) 26 | * * accept `string` values, 27 | * * all other values are automatically converted to `string` (a warning will be logged but evaluation will continue as normal). 28 | * 29 | * **SemVer-based comparators** (IS ONE OF, <, >=, etc.) 30 | * * accept `string` values containing a properly formatted, valid semver value, 31 | * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 32 | * 33 | * **Number-based comparators** (=, <, >=, etc.) 34 | * * accept `number` values, 35 | * * accept `string` values containing a properly formatted, valid `number` value, 36 | * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 37 | * 38 | * **Date time-based comparators** (BEFORE / AFTER) 39 | * * accept `Date` values, which are automatically converted to a second-based Unix timestamp, 40 | * * accept `number` values representing a second-based Unix timestamp, 41 | * * accept `string` values containing a properly formatted, valid `number` value, 42 | * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 43 | * 44 | * **String array-based comparators** (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF) 45 | * * accept arrays of `string`, 46 | * * accept `string` values containing a valid JSON string which can be deserialized to an array of `string`, 47 | * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 48 | **/ 49 | public custom: { [key: string]: UserAttributeValue } = {} 50 | ) { 51 | } 52 | } 53 | 54 | // NOTE: These functions could be instance methods of the User class, however formerly we suggested `const user = { ... }`-style initialization in the SDK docs, 55 | // which would lead to "...is not a function" errors if we called functions on instances created that way as those don't have the correct prototype. 56 | 57 | export function getUserAttribute(user: User, name: string): UserAttributeValue | null | undefined { 58 | switch (name) { 59 | case "Identifier": return user.identifier ?? ""; 60 | case "Email": return user.email; 61 | case "Country": return user.country; 62 | default: return user.custom?.[name]; 63 | } 64 | } 65 | 66 | export function getUserAttributes(user: User): { [key: string]: UserAttributeValue } { 67 | 68 | const result: { [key: string]: UserAttributeValue } = {}; 69 | 70 | const identifierAttribute: WellKnownUserObjectAttribute = "Identifier"; 71 | const emailAttribute: WellKnownUserObjectAttribute = "Email"; 72 | const countryAttribute: WellKnownUserObjectAttribute = "Country"; 73 | 74 | result[identifierAttribute] = user.identifier ?? ""; 75 | 76 | if (user.email != null) { 77 | result[emailAttribute] = user.email; 78 | } 79 | 80 | if (user.country != null) { 81 | result[countryAttribute] = user.country; 82 | } 83 | 84 | if (user.custom != null) { 85 | const wellKnownAttributes: string[] = [identifierAttribute, emailAttribute, countryAttribute]; 86 | for (const [attributeName, attributeValue] of Object.entries(user.custom)) { 87 | if (attributeValue != null && wellKnownAttributes.indexOf(attributeName) < 0) { 88 | result[attributeName] = attributeValue; 89 | } 90 | } 91 | } 92 | 93 | return result; 94 | } 95 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | // NOTE: Normally, we'd just use AbortController/AbortSignal, however that may not be available on all platforms, 2 | // and we don't want to include a complete polyfill. So we implement a simplified version that fits our use case. 3 | export class AbortToken { 4 | private callbacks: (() => void)[] | null = []; 5 | get aborted(): boolean { return !this.callbacks; } 6 | 7 | abort(): void { 8 | if (!this.aborted) { 9 | const callbacks = this.callbacks!; 10 | this.callbacks = null; 11 | for (const callback of callbacks) { 12 | callback(); 13 | } 14 | } 15 | } 16 | 17 | registerCallback(callback: () => void): () => void { 18 | if (this.aborted) { 19 | callback(); 20 | return () => { }; 21 | } 22 | 23 | this.callbacks!.push(callback); 24 | return () => { 25 | const callbacks = this.callbacks; 26 | let index: number; 27 | if (callbacks && (index = callbacks.indexOf(callback)) >= 0) { 28 | callbacks.splice(index, 1); 29 | } 30 | }; 31 | } 32 | } 33 | 34 | export function delay(delayMs: number, abortToken?: AbortToken | null): Promise { 35 | let timerId: ReturnType; 36 | return new Promise(resolve => { 37 | const unregisterAbortCallback = abortToken?.registerCallback(() => { 38 | clearTimeout(timerId); 39 | resolve(false); 40 | }); 41 | 42 | timerId = setTimeout(() => { 43 | unregisterAbortCallback?.(); 44 | resolve(true); 45 | }, delayMs); 46 | }); 47 | } 48 | 49 | export const getMonotonicTimeMs = typeof performance !== "undefined" && typeof performance.now === "function" 50 | ? () => performance.now() 51 | : () => new Date().getTime(); 52 | 53 | /** Formats error in a similar way to Chromium-based browsers. */ 54 | export function errorToString(err: any, includeStackTrace = false): string { 55 | return err instanceof Error ? visit(err, "") : "" + err; 56 | 57 | function visit(err: Error, indent: string, visited?: Error[]) { 58 | const errString = err.toString(); 59 | let s = (!indent ? indent : indent.substring(4) + "--> ") + errString; 60 | if (includeStackTrace && err.stack) { 61 | let stack = err.stack.trim(); 62 | // NOTE: Some JS runtimes (e.g. V8) includes the error in the stack trace, some don't (e.g. SpiderMonkey). 63 | if (stack.lastIndexOf(errString, 0) === 0) { 64 | stack = stack.substring(errString.length).trim(); 65 | } 66 | s += "\n" + stack.replace(/^\s*(?:at\s)?/gm, indent + " at "); 67 | } 68 | 69 | if (typeof AggregateError !== "undefined" && err instanceof AggregateError) { 70 | (visited ??= []).push(err); 71 | for (const innerErr of err.errors) { 72 | if (innerErr instanceof Error) { 73 | if (visited.indexOf(innerErr) >= 0) { 74 | continue; 75 | } 76 | s += "\n" + visit(innerErr, indent + " ", visited); 77 | } 78 | else { 79 | s += "\n" + indent + "--> " + innerErr; 80 | } 81 | } 82 | visited.pop(); 83 | } 84 | 85 | return s; 86 | } 87 | } 88 | 89 | export function throwError(err: any): never { 90 | throw err; 91 | } 92 | 93 | export function isObject(value: unknown): value is Record { 94 | return value !== null && typeof value === "object" && !isArray(value); 95 | } 96 | 97 | export function isArray(value: unknown): value is readonly unknown[] { 98 | // See also: https://github.com/microsoft/TypeScript/issues/17002#issuecomment-1477626624 99 | return Array.isArray(value); 100 | } 101 | 102 | export function isStringArray(value: unknown): value is string[] { 103 | return isArray(value) && !value.some(item => typeof item !== "string"); 104 | } 105 | 106 | export function formatStringList(items: ReadonlyArray, maxLength = 0, getOmittedItemsText?: (count: number) => string, separator = ", "): string { 107 | const length = items.length; 108 | if (!length) { 109 | return ""; 110 | } 111 | 112 | let appendix = ""; 113 | 114 | if (maxLength > 0 && length > maxLength) { 115 | items = items.slice(0, maxLength); 116 | if (getOmittedItemsText) { 117 | appendix = getOmittedItemsText(length - maxLength); 118 | } 119 | } 120 | 121 | return "'" + items.join("'" + separator + "'") + "'" + appendix; 122 | } 123 | 124 | export function isPromiseLike(obj: unknown): obj is PromiseLike { 125 | // See also: https://stackoverflow.com/a/27746324/8656352 126 | return typeof (obj as PromiseLike)?.then === "function"; 127 | } 128 | 129 | export function utf8Encode(text: string): string { 130 | function codePointAt(text: string, index: number): number { 131 | const ch = text.charCodeAt(index); 132 | if (0xD800 <= ch && ch < 0xDC00) { // is high surrogate? 133 | const nextCh = text.charCodeAt(index + 1); 134 | if (0xDC00 <= nextCh && nextCh <= 0xDFFF) { // is low surrogate? 135 | return (ch << 10) + nextCh - 0x35FDC00; 136 | } 137 | } 138 | return ch; 139 | } 140 | 141 | let utf8text = "", chunkStart = 0; 142 | const fromCharCode = String.fromCharCode; 143 | 144 | let i; 145 | for (i = 0; i < text.length; i++) { 146 | const cp = codePointAt(text, i); 147 | if (cp <= 0x7F) { 148 | continue; 149 | } 150 | 151 | // See also: https://stackoverflow.com/a/6240184/8656352 152 | 153 | utf8text += text.slice(chunkStart, i); 154 | if (cp <= 0x7FF) { 155 | utf8text += fromCharCode(0xC0 | (cp >> 6)); 156 | utf8text += fromCharCode(0x80 | (cp & 0x3F)); 157 | } 158 | else if (cp <= 0xFFFF) { 159 | utf8text += fromCharCode(0xE0 | (cp >> 12)); 160 | utf8text += fromCharCode(0x80 | ((cp >> 6) & 0x3F)); 161 | utf8text += fromCharCode(0x80 | (cp & 0x3F)); 162 | } 163 | else { 164 | utf8text += fromCharCode(0xF0 | (cp >> 18)); 165 | utf8text += fromCharCode(0x80 | ((cp >> 12) & 0x3F)); 166 | utf8text += fromCharCode(0x80 | ((cp >> 6) & 0x3F)); 167 | utf8text += fromCharCode(0x80 | (cp & 0x3F)); 168 | ++i; 169 | } 170 | chunkStart = i + 1; 171 | } 172 | 173 | return utf8text += text.slice(chunkStart, i); 174 | } 175 | 176 | export function parseFloatStrict(value: unknown): number { 177 | // NOTE: JS's float to string conversion is too forgiving, it accepts hex numbers and ignores invalid characters after the number. 178 | 179 | if (typeof value === "number") { 180 | return value; 181 | } 182 | 183 | if (typeof value !== "string" || !value.length || /^\s*$|^\s*0[^\d.eE]/.test(value)) { 184 | return NaN; 185 | } 186 | 187 | return +value; 188 | } 189 | 190 | export function shallowClone(obj: T, propertyReplacer?: (key: keyof T, value: unknown) => unknown): Record { 191 | const clone = {} as Record; 192 | for (const key in obj) { 193 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 194 | const value = obj[key]; 195 | clone[key] = propertyReplacer ? propertyReplacer(key, value) : value; 196 | } 197 | } 198 | return clone; 199 | } 200 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { IConfigCatClient, IConfigCatKernel } from "./ConfigCatClient"; 2 | import { ConfigCatClient } from "./ConfigCatClient"; 3 | import type { IAutoPollOptions, ILazyLoadingOptions, IManualPollOptions, OptionsForPollingMode } from "./ConfigCatClientOptions"; 4 | import { PollingMode } from "./ConfigCatClientOptions"; 5 | import type { IConfigCatLogger } from "./ConfigCatLogger"; 6 | import { ConfigCatConsoleLogger, LogLevel } from "./ConfigCatLogger"; 7 | import { FlagOverrides, MapOverrideDataSource, OverrideBehaviour } from "./FlagOverrides"; 8 | import { setupPolyfills } from "./Polyfills"; 9 | import type { SettingValue } from "./ProjectConfig"; 10 | 11 | setupPolyfills(); 12 | 13 | /** 14 | * Returns an instance of `ConfigCatClient` for the specified SDK Key. 15 | * @remarks This method returns a single, shared instance per each distinct SDK Key. 16 | * That is, a new client object is created only when there is none available for the specified SDK Key. 17 | * Otherwise, the already created instance is returned (in which case the `pollingMode`, `options` and `configCatKernel` arguments are ignored). 18 | * So, please keep in mind that when you make multiple calls to this method using the same SDK Key, you may end up with multiple references to the same client object. 19 | * @param sdkKey SDK Key to access the ConfigCat config. 20 | * @param pollingMode The polling mode to use. 21 | * @param options Options for the specified polling mode. 22 | */ 23 | export function getClient(sdkKey: string, pollingMode: TMode, options: OptionsForPollingMode | undefined | null, configCatKernel: IConfigCatKernel): IConfigCatClient { 24 | return ConfigCatClient.get(sdkKey, pollingMode, options, configCatKernel); 25 | } 26 | 27 | /** 28 | * Disposes all existing `ConfigCatClient` instances. 29 | */ 30 | export function disposeAllClients(): void { 31 | ConfigCatClient.disposeAll(); 32 | } 33 | 34 | /** 35 | * Creates an instance of `ConfigCatConsoleLogger`. 36 | * @param logLevel Log level (the minimum level to use for filtering log events). 37 | * @param eol The character sequence to use for line breaks in log messages. Defaults to "\n". 38 | */ 39 | export function createConsoleLogger(logLevel: LogLevel, eol?: string): IConfigCatLogger { 40 | return new ConfigCatConsoleLogger(logLevel, eol); 41 | } 42 | 43 | /** 44 | * Creates an instance of `FlagOverrides` that uses a map data source. 45 | * @param map The map that contains the overrides. 46 | * @param behaviour The override behaviour. 47 | * Specifies whether the local values should override the remote values 48 | * or local values should only be used when a remote value doesn't exist 49 | * or the local values should be used only. 50 | * @param watchChanges If set to `true`, the input map will be tracked for changes. 51 | */ 52 | export function createFlagOverridesFromMap(map: { [name: string]: NonNullable }, behaviour: OverrideBehaviour, watchChanges?: boolean): FlagOverrides { 53 | return new FlagOverrides(new MapOverrideDataSource(map, watchChanges), behaviour); 54 | } 55 | 56 | /* Public types for platform-specific SDKs */ 57 | 58 | // List types here which are required to implement the platform-specific SDKs but shouldn't be exposed to end users. 59 | 60 | export type { IConfigCatKernel }; 61 | 62 | export type { IConfigFetcher, IFetchResponse, FetchErrorCauses } from "./ConfigFetcher"; 63 | 64 | export { FetchStatus, FetchResult, FetchError } from "./ConfigFetcher"; 65 | 66 | export type { OptionsBase } from "./ConfigCatClientOptions"; 67 | 68 | export type { IConfigCache } from "./ConfigCatCache"; 69 | 70 | export { InMemoryConfigCache, ExternalConfigCache } from "./ConfigCatCache"; 71 | 72 | export type { IEventProvider, IEventEmitter } from "./EventEmitter"; 73 | 74 | /* Public types for end users */ 75 | 76 | // List types here which are part of the public API of platform-specific SDKs, thus, should be exposed to end users. 77 | // These exports should be re-exported in the entry module of each platform-specific SDK! 78 | 79 | export { PollingMode }; 80 | 81 | export type { IOptions } from "./ConfigCatClientOptions"; 82 | 83 | export type { IAutoPollOptions, IManualPollOptions, ILazyLoadingOptions }; 84 | 85 | export { DataGovernance } from "./ConfigCatClientOptions"; 86 | 87 | export type { IConfigCatLogger }; 88 | 89 | export type { LogEventId, LogMessage } from "./ConfigCatLogger"; 90 | 91 | export { LogLevel }; 92 | 93 | export { FormattableLogMessage } from "./ConfigCatLogger"; 94 | 95 | export type { IConfigCatCache } from "./ConfigCatCache"; 96 | 97 | export type { 98 | IConfig, ISegment, SettingTypeMap, SettingValue, VariationIdValue, ISettingValueContainer, ISettingUnion, ISetting, ITargetingRule, IPercentageOption, 99 | ConditionTypeMap, IConditionUnion, ICondition, UserConditionComparisonValueTypeMap, IUserConditionUnion, IUserCondition, IPrerequisiteFlagCondition, ISegmentCondition 100 | } from "./ProjectConfig"; 101 | 102 | export { SettingType, UserComparator, PrerequisiteFlagComparator, SegmentComparator } from "./ConfigJson"; 103 | 104 | export type { IConfigCatClient }; 105 | 106 | export type { IConfigCatClientSnapshot } from "./ConfigCatClient"; 107 | 108 | export { SettingKeyValue } from "./ConfigCatClient"; 109 | 110 | export type { IEvaluationDetails, SettingTypeOf } from "./RolloutEvaluator"; 111 | 112 | export type { UserAttributeValue } from "./User"; 113 | 114 | export { User } from "./User"; 115 | 116 | export type { FlagOverrides }; 117 | 118 | export { OverrideBehaviour }; 119 | 120 | export { ClientCacheState, RefreshResult } from "./ConfigServiceBase"; 121 | 122 | export type { IProvidesHooks, HookEvents } from "./Hooks"; 123 | -------------------------------------------------------------------------------- /src/lib.es2021.promise.d.ts: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 8 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 9 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 10 | MERCHANTABLITY OR NON-INFRINGEMENT. 11 | 12 | See the Apache Version 2.0 License for specific language governing permissions 13 | and limitations under the License. 14 | ***************************************************************************** */ 15 | 16 | 17 | 18 | /// 19 | 20 | 21 | interface AggregateError extends Error { 22 | errors: any[] 23 | } 24 | 25 | interface AggregateErrorConstructor { 26 | new(errors: Iterable, message?: string): AggregateError; 27 | (errors: Iterable, message?: string): AggregateError; 28 | readonly prototype: AggregateError; 29 | } 30 | 31 | declare var AggregateError: AggregateErrorConstructor; 32 | -------------------------------------------------------------------------------- /src/lib.es2021.weakref.d.ts: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 8 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 9 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 10 | MERCHANTABLITY OR NON-INFRINGEMENT. 11 | 12 | See the Apache Version 2.0 License for specific language governing permissions 13 | and limitations under the License. 14 | ***************************************************************************** */ 15 | 16 | 17 | 18 | /// 19 | 20 | 21 | interface WeakRef { 22 | readonly [Symbol.toStringTag]: "WeakRef"; 23 | 24 | /** 25 | * Returns the WeakRef instance's target object, or undefined if the target object has been 26 | * reclaimed. 27 | */ 28 | deref(): T | undefined; 29 | } 30 | 31 | interface WeakRefConstructor { 32 | readonly prototype: WeakRef; 33 | 34 | /** 35 | * Creates a WeakRef instance for the given target object. 36 | * @param target The target object for the WeakRef instance. 37 | */ 38 | new(target: T): WeakRef; 39 | } 40 | 41 | declare var WeakRef: WeakRefConstructor; 42 | 43 | interface FinalizationRegistry { 44 | readonly [Symbol.toStringTag]: "FinalizationRegistry"; 45 | 46 | /** 47 | * Registers an object with the registry. 48 | * @param target The target object to register. 49 | * @param heldValue The value to pass to the finalizer for this object. This cannot be the 50 | * target object. 51 | * @param unregisterToken The token to pass to the unregister method to unregister the target 52 | * object. If provided (and not undefined), this must be an object. If not provided, the target 53 | * cannot be unregistered. 54 | */ 55 | register(target: object, heldValue: T, unregisterToken?: object): void; 56 | 57 | /** 58 | * Unregisters an object from the registry. 59 | * @param unregisterToken The token that was used as the unregisterToken argument when calling 60 | * register to register the target object. 61 | */ 62 | unregister(unregisterToken: object): void; 63 | } 64 | 65 | interface FinalizationRegistryConstructor { 66 | readonly prototype: FinalizationRegistry; 67 | 68 | /** 69 | * Creates a finalization registry with an associated cleanup callback 70 | * @param cleanupCallback The callback to call after an object in the registry has been reclaimed. 71 | */ 72 | new(cleanupCallback: (heldValue: T) => void): FinalizationRegistry; 73 | } 74 | 75 | declare var FinalizationRegistry: FinalizationRegistryConstructor; 76 | -------------------------------------------------------------------------------- /test/ConfigCatCacheTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { ExternalConfigCache, IConfigCache, InMemoryConfigCache, } from "../src/ConfigCatCache"; 4 | import { ManualPollOptions } from "../src/ConfigCatClientOptions"; 5 | import { LogLevel, LoggerWrapper } from "../src/ConfigCatLogger"; 6 | import { Config, ProjectConfig } from "../src/ProjectConfig"; 7 | import { FakeExternalCache, FakeLogger, FaultyFakeExternalCache } from "./helpers/fakes"; 8 | 9 | describe("ConfigCatCache", () => { 10 | for (const isExternal of [false, true]) { 11 | it(`${isExternal ? ExternalConfigCache.prototype.constructor.name : InMemoryConfigCache.prototype.constructor.name} works`, async () => { 12 | const cacheKey = ""; 13 | 14 | let externalCache: FakeExternalCache | undefined; 15 | const [configCache, getLocalCachedConfig] = isExternal 16 | ? [ 17 | new ExternalConfigCache(externalCache = new FakeExternalCache(), new LoggerWrapper(new FakeLogger())), 18 | (cache: IConfigCache) => (cache as ExternalConfigCache)["cachedConfig"] 19 | ] 20 | : [ 21 | new InMemoryConfigCache(), 22 | (cache: IConfigCache) => (cache as InMemoryConfigCache)["cachedConfig"] 23 | ]; 24 | 25 | // 1. Cache should return the empty config initially 26 | let cachedConfig = await configCache.get(cacheKey); 27 | assert.equal(ProjectConfig.empty, cachedConfig); 28 | 29 | // 2. When cache is empty, setting an empty config with newer timestamp should overwrite the cache (but only locally!) 30 | const config2 = ProjectConfig.empty.with(ProjectConfig.generateTimestamp()); 31 | await configCache.set(cacheKey, config2); 32 | cachedConfig = await configCache.get(cacheKey); 33 | 34 | assert.equal(config2, cachedConfig); 35 | assert.equal(config2, getLocalCachedConfig(configCache)); 36 | if (externalCache) { 37 | assert.isUndefined(externalCache.cachedValue); 38 | } 39 | 40 | // 3. When cache is empty, setting a non-empty config with any (even older) timestamp should overwrite the cache. 41 | const configJson = "{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}"; 42 | const config3 = new ProjectConfig(configJson, Config.deserialize(configJson), config2.timestamp - 1000, "\"ETAG\""); 43 | await configCache.set(cacheKey, config3); 44 | cachedConfig = await configCache.get(cacheKey); 45 | 46 | assert.equal(config3, cachedConfig); 47 | assert.equal(config3, getLocalCachedConfig(configCache)); 48 | if (externalCache) { 49 | assert.isDefined(externalCache.cachedValue); 50 | } 51 | }); 52 | } 53 | 54 | it(`${ExternalConfigCache.prototype.constructor.name} should handle when external cache fails`, async () => { 55 | const cacheKey = ""; 56 | 57 | const logger = new FakeLogger(LogLevel.Warn); 58 | 59 | const externalCache = new FaultyFakeExternalCache(); 60 | const configCache = new ExternalConfigCache(externalCache, new LoggerWrapper(logger)); 61 | 62 | // 1. Initial read should return the empty config. 63 | let cachedConfig = await configCache.get(cacheKey); 64 | 65 | assert.equal(ProjectConfig.empty, cachedConfig); 66 | 67 | assert.equal(1, logger.events.filter(([level, eventId, _, err]) => 68 | level === LogLevel.Error && eventId === 2200 && (err as Error).message === "Operation failed :(").length); 69 | 70 | // 2. Set should overwrite the local cache and log the error. 71 | 72 | logger.events.length = 0; 73 | 74 | const configJson = "{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}"; 75 | const config = new ProjectConfig(configJson, Config.deserialize(configJson), ProjectConfig.generateTimestamp(), "\"ETAG\""); 76 | 77 | await configCache.set(cacheKey, config); 78 | 79 | assert.equal(config, configCache["cachedConfig"]); 80 | 81 | assert.equal(1, logger.events.filter(([level, eventId, _, err]) => 82 | level === LogLevel.Error && eventId === 2201 && (err as Error).message === "Operation failed :(").length); 83 | 84 | // 3. Get should log the error and return the local cache which was set previously. 85 | 86 | logger.events.length = 0; 87 | 88 | cachedConfig = await configCache.get(cacheKey); 89 | 90 | assert.equal(config, cachedConfig); 91 | 92 | assert.equal(1, logger.events.filter(([level, eventId, _, err]) => 93 | level === LogLevel.Error && eventId === 2200 && (err as Error).message === "Operation failed :(").length); 94 | }); 95 | 96 | for (const [sdkKey, expectedCacheKey] of [ 97 | ["configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012", "f83ba5d45bceb4bb704410f51b704fb6dfa19942"], 98 | ["configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012", "da7bfd8662209c8ed3f9db96daed4f8d91ba5876"], 99 | ]) { 100 | it(`Cache key generation should be platform independent - ${sdkKey}`, () => { 101 | const options = new ManualPollOptions(sdkKey, "common", "1.0.0"); 102 | assert.strictEqual(options.getCacheKey(), expectedCacheKey); 103 | }); 104 | } 105 | 106 | const payloadTestConfigJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0},\"f\":{\"testKey\":{\"v\":\"testValue\",\"t\":1,\"p\":[],\"r\":[]}}}"; 107 | for (const [configJson, timestamp, httpETag, expectedPayload] of [ 108 | [payloadTestConfigJson, "2023-06-14T15:27:15.8440000Z", "test-etag", "1686756435844\ntest-etag\n" + payloadTestConfigJson], 109 | ]) { 110 | it(`Cache payload serialization should be platform independent - ${httpETag}`, () => { 111 | const pc = new ProjectConfig(configJson, JSON.parse(configJson), Date.parse(timestamp), httpETag); 112 | assert.strictEqual(ProjectConfig.serialize(pc), expectedPayload); 113 | }); 114 | } 115 | }); 116 | -------------------------------------------------------------------------------- /test/ConfigCatLoggerTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { IConfigCatLogger, LogEventId, LogLevel, LogMessage, LoggerWrapper } from "../src/ConfigCatLogger"; 4 | 5 | describe("ConfigCatLogger", () => { 6 | for (const level of Object.values(LogLevel).filter(key => typeof key === "number") as LogLevel[]) { 7 | it(`Logging works with level ${LogLevel[level]}`, () => { 8 | const messages: [LogLevel, LogEventId, LogMessage, any][] = []; 9 | 10 | const loggerImpl = new class implements IConfigCatLogger { 11 | level = level; 12 | log(level: LogLevel, eventId: LogEventId, message: LogMessage, exception?: any) { 13 | messages.push([level, eventId, message, exception]); 14 | } 15 | }; 16 | 17 | const logger = new LoggerWrapper(loggerImpl); 18 | const err = new Error(); 19 | 20 | logger.log(LogLevel.Debug, 0, `${LogLevel[LogLevel.Debug]} message`); 21 | logger.log(LogLevel.Info, 1, `${LogLevel[LogLevel.Info]} message`); 22 | logger.log(LogLevel.Warn, 2, `${LogLevel[LogLevel.Warn]} message`); 23 | logger.log(LogLevel.Error, 3, `${LogLevel[LogLevel.Error]} message`, err); 24 | 25 | let expectedCount = 0; 26 | if (level >= LogLevel.Debug) { 27 | assert.equal(messages.filter(([level, eventId, msg, ex]) => level === LogLevel.Debug && eventId === 0 && msg === `${LogLevel[level]} message` && ex === void 0).length, 1); 28 | expectedCount++; 29 | } 30 | 31 | if (level >= LogLevel.Info) { 32 | assert.equal(messages.filter(([level, eventId, msg, ex]) => level === LogLevel.Info && eventId === 1 && msg === `${LogLevel[level]} message` && ex === void 0).length, 1); 33 | expectedCount++; 34 | } 35 | 36 | if (level >= LogLevel.Warn) { 37 | assert.equal(messages.filter(([level, eventId, msg, ex]) => level === LogLevel.Warn && eventId === 2 && msg === `${LogLevel[level]} message` && ex === void 0).length, 1); 38 | expectedCount++; 39 | } 40 | 41 | if (level >= LogLevel.Error) { 42 | assert.equal(messages.filter(([level, eventId, msg, ex]) => level === LogLevel.Error && eventId === 3 && msg === `${LogLevel[level]} message` && ex === err).length, 1); 43 | expectedCount++; 44 | } 45 | 46 | assert.equal(messages.length, expectedCount); 47 | }); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /test/EvaluationLogTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as util from "util"; 5 | import "mocha"; 6 | import { User } from "../src"; 7 | import { LogLevel, LoggerWrapper } from "../src/ConfigCatLogger"; 8 | import { SettingValue } from "../src/ProjectConfig"; 9 | import { RolloutEvaluator, evaluate } from "../src/RolloutEvaluator"; 10 | import { WellKnownUserObjectAttribute } from "../src/User"; 11 | import { errorToString } from "../src/Utils"; 12 | import { CdnConfigLocation, ConfigLocation, LocalFileConfigLocation } from "./helpers/ConfigLocation"; 13 | import { FakeLogger } from "./helpers/fakes"; 14 | import { normalizeLineEndings } from "./helpers/utils"; 15 | 16 | const testDataBasePath = path.join("test", "data", "evaluationlog"); 17 | 18 | type TestSet = { 19 | sdkKey: string; 20 | baseUrl?: string; 21 | jsonOverride?: string; 22 | tests?: ReadonlyArray; 23 | } 24 | 25 | type TestCase = { 26 | key: string; 27 | defaultValue: SettingValue; 28 | returnValue: NonNullable; 29 | expectedLog: string; 30 | user?: Readonly<{ [key: string]: string }>; 31 | } 32 | 33 | describe("Evaluation log", () => { 34 | describeTestSet("simple_value"); 35 | describeTestSet("1_targeting_rule"); 36 | describeTestSet("2_targeting_rules"); 37 | describeTestSet("options_based_on_user_id"); 38 | describeTestSet("options_based_on_custom_attr"); 39 | describeTestSet("options_after_targeting_rule"); 40 | describeTestSet("options_within_targeting_rule"); 41 | describeTestSet("and_rules"); 42 | describeTestSet("segment"); 43 | describeTestSet("prerequisite_flag"); 44 | describeTestSet("comparators"); 45 | describeTestSet("epoch_date_validation"); 46 | describeTestSet("number_validation"); 47 | describeTestSet("semver_validation"); 48 | describeTestSet("list_truncation"); 49 | }); 50 | 51 | function describeTestSet(testSetName: string) { 52 | for (const [configLocation, testCase] of getTestCases(testSetName)) { 53 | const userJson = JSON.stringify(testCase.user ?? null).replace(/"/g, "'"); 54 | it(`${testSetName} - ${configLocation} | ${testCase.key} | ${testCase.defaultValue} | ${userJson}`, () => runTest(testSetName, configLocation, testCase)); 55 | } 56 | } 57 | 58 | function* getTestCases(testSetName: string): Generator<[ConfigLocation, TestCase], void, undefined> { 59 | const data = fs.readFileSync(path.join(testDataBasePath, testSetName + ".json"), "utf8"); 60 | const testSet: TestSet = JSON.parse(data); 61 | 62 | const configLocation = testSet.sdkKey 63 | ? new CdnConfigLocation(testSet.sdkKey, testSet.baseUrl) 64 | : new LocalFileConfigLocation(testDataBasePath, "_overrides", testSet.jsonOverride!); 65 | 66 | for (const testCase of testSet.tests ?? []) { 67 | yield [configLocation, testCase]; 68 | } 69 | } 70 | 71 | function createUser(userRaw?: Readonly<{ [key: string]: string }>): User | undefined { 72 | if (!userRaw) { 73 | return; 74 | } 75 | 76 | const identifierAttribute: WellKnownUserObjectAttribute = "Identifier"; 77 | const emailAttribute: WellKnownUserObjectAttribute = "Email"; 78 | const countryAttribute: WellKnownUserObjectAttribute = "Country"; 79 | 80 | const user = new User(userRaw[identifierAttribute]); 81 | 82 | const email = userRaw[emailAttribute]; 83 | if (email) { 84 | user.email = email; 85 | } 86 | 87 | const country = userRaw[countryAttribute]; 88 | if (country) { 89 | user.country = country; 90 | } 91 | 92 | const wellKnownAttributes: string[] = [identifierAttribute, emailAttribute, countryAttribute]; 93 | for (const attributeName of Object.keys(userRaw)) { 94 | if (wellKnownAttributes.indexOf(attributeName) < 0) { 95 | user.custom[attributeName] = userRaw[attributeName]; 96 | } 97 | } 98 | 99 | return user; 100 | } 101 | 102 | function formatLogEvent(event: FakeLogger["events"][0]) { 103 | const [level, eventId, message, exception] = event; 104 | 105 | const levelString = 106 | level === LogLevel.Debug ? "DEBUG" : 107 | level === LogLevel.Info ? "INFO" : 108 | level === LogLevel.Warn ? "WARNING" : 109 | level === LogLevel.Error ? "ERROR" : 110 | LogLevel[level].toUpperCase().padStart(5); 111 | 112 | const exceptionString = exception !== void 0 ? "\n" + errorToString(exception, true) : ""; 113 | 114 | return `${levelString} [${eventId}] ${message}${exceptionString}`; 115 | } 116 | 117 | async function runTest(testSetName: string, configLocation: ConfigLocation, testCase: TestCase) { 118 | const config = await configLocation.fetchConfigCachedAsync(); 119 | 120 | const fakeLogger = new FakeLogger(); 121 | const logger = new LoggerWrapper(fakeLogger); 122 | const evaluator = new RolloutEvaluator(logger); 123 | 124 | const user = createUser(testCase.user); 125 | const evaluationDetails = evaluate(evaluator, config.settings, testCase.key, testCase.defaultValue, user, null, logger); 126 | const actualReturnValue = evaluationDetails.value; 127 | 128 | assert.strictEqual(actualReturnValue, testCase.returnValue); 129 | 130 | const expectedLogFilePath = path.join(testDataBasePath, testSetName, testCase.expectedLog); 131 | const expectedLogText = normalizeLineEndings(await util.promisify(fs.readFile)(expectedLogFilePath, "utf8")).replace(/(\r|\n)*$/, ""); 132 | const actualLogText = fakeLogger.events.map(e => formatLogEvent(e)).join("\n"); 133 | 134 | assert.strictEqual(actualLogText, expectedLogText); 135 | } 136 | -------------------------------------------------------------------------------- /test/HashTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { sha1, sha256 } from "../src/Hash"; 4 | import { utf8Encode } from "../src/Utils"; 5 | 6 | describe("Hash functions", () => { 7 | for (const [input, expectedOutput] of [ 8 | ["", "da39a3ee5e6b4b0d3255bfef95601890afd80709"], 9 | ["abc", "a9993e364706816aba3e25717850c26c9cd0d89d"], 10 | ["<árvíztűrő tükörfúrógép | ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP>", "1be0a50a536668a6ff6c9650e13c2c09f011f3d8"], 11 | ["\u{0} \u{7F}\n\u{80} \u{7FF}\n\u{800} \u{FFFF}\n\u{10000} \u{10FFFF}\r\n", "b7cd3a8fe37c968f87d9d2eb581a42c94952a890"], 12 | ]) { 13 | it(`sha1 should work - input: '${input}'`, () => { 14 | let actualOutput = sha1(input); 15 | assert.equal(actualOutput, expectedOutput); 16 | actualOutput = sha1(input); 17 | assert.equal(actualOutput, expectedOutput); 18 | }); 19 | } 20 | 21 | for (const [input, expectedOutput] of [ 22 | ["", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"], 23 | ["abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"], 24 | ["<árvíztűrő tükörfúrógép | ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP>", "8a396060d6d54c97112c2d420144d528f4b1d824c66a83b54070b0f0bb8cafa7"], 25 | ["\u{0} \u{7F}\n\u{80} \u{7FF}\n\u{800} \u{FFFF}\n\u{10000} \u{10FFFF}\r\n", "306979857214e80b4eee7caf751d38c7491147fa62ed2980f4fd6d7398479b9b"], 26 | ]) { 27 | it(`sha256 should work - input: '${input}'`, () => { 28 | let actualOutput = sha256(utf8Encode(input)); 29 | assert.equal(actualOutput, expectedOutput); 30 | actualOutput = sha256(utf8Encode(input)); 31 | assert.equal(actualOutput, expectedOutput); 32 | }); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /test/IndexTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { PollingMode } from "../src"; 4 | import { IConfigCatClient } from "../src/ConfigCatClient"; 5 | import * as configcatClient from "../src/index"; 6 | import { FakeConfigCatKernel, FakeConfigFetcher } from "./helpers/fakes"; 7 | 8 | describe("ConfigCatClient index (main)", () => { 9 | 10 | it("getClient ShouldCreateInstance - AutoPoll", () => { 11 | 12 | const configCatKernel: FakeConfigCatKernel = { configFetcher: new FakeConfigFetcher(), sdkType: "common", sdkVersion: "1.0.0" }; 13 | const client: IConfigCatClient = configcatClient.getClient("SDKKEY-890123456789012/1234567890123456789012", PollingMode.AutoPoll, void 0, configCatKernel); 14 | 15 | try { 16 | assert.isDefined(client); 17 | } 18 | finally { 19 | client.dispose(); 20 | } 21 | }); 22 | 23 | it("getClient ShouldCreateInstance - LazyLoad", () => { 24 | 25 | const configCatKernel: FakeConfigCatKernel = { configFetcher: new FakeConfigFetcher(), sdkType: "common", sdkVersion: "1.0.0" }; 26 | const client: IConfigCatClient = configcatClient.getClient("SDKKEY-890123456789012/1234567890123456789012", PollingMode.LazyLoad, void 0, configCatKernel); 27 | 28 | try { 29 | assert.isDefined(client); 30 | } 31 | finally { 32 | client.dispose(); 33 | } 34 | }); 35 | 36 | it("getClient ShouldCreateInstance - ManualPoll", () => { 37 | 38 | const configCatKernel: FakeConfigCatKernel = { configFetcher: new FakeConfigFetcher(), sdkType: "common", sdkVersion: "1.0.0" }; 39 | const client: IConfigCatClient = configcatClient.getClient("SDKKEY-890123456789012/1234567890123456789012", PollingMode.ManualPoll, void 0, configCatKernel); 40 | 41 | try { 42 | assert.isDefined(client); 43 | } 44 | finally { 45 | client.dispose(); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/PolyfillTests.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import "mocha"; 3 | import { ObjectEntriesPolyfill, ObjectFromEntriesPolyfill, ObjectValuesPolyfill, getWeakRefStub } from "../src/Polyfills"; 4 | 5 | describe("Polyfills", () => { 6 | it("Object.values polyfill should work", () => { 7 | assert.deepEqual([], ObjectValuesPolyfill("" as any)); 8 | 9 | assert.deepEqual([], ObjectValuesPolyfill([])); 10 | // eslint-disable-next-line no-sparse-arrays 11 | expect(ObjectValuesPolyfill([1, , "b"])).to.have.members([1, "b"]); 12 | 13 | assert.deepEqual([], ObjectValuesPolyfill({})); 14 | // eslint-disable-next-line @typescript-eslint/naming-convention 15 | expect(ObjectValuesPolyfill({ "a": 1, 2: "b" })).to.have.members([1, "b"]); 16 | }); 17 | 18 | it("Object.entries polyfill should work", () => { 19 | assert.deepEqual([], ObjectEntriesPolyfill("" as any)); 20 | 21 | assert.deepEqual([], ObjectEntriesPolyfill([])); 22 | // eslint-disable-next-line no-sparse-arrays 23 | expect(ObjectEntriesPolyfill([1, , "b"])).to.have.deep.members([["0", 1], ["2", "b"]]); 24 | 25 | assert.deepEqual([], ObjectEntriesPolyfill({})); 26 | // eslint-disable-next-line @typescript-eslint/naming-convention 27 | expect(ObjectEntriesPolyfill({ "a": 1, 2: "b" })).to.have.deep.members([["a", 1], ["2", "b"]]); 28 | }); 29 | 30 | it("Object.fromEntries polyfill should work", () => { 31 | assert.deepEqual({}, ObjectFromEntriesPolyfill("" as any)); 32 | 33 | assert.deepEqual({}, ObjectFromEntriesPolyfill([])); 34 | // eslint-disable-next-line @typescript-eslint/naming-convention 35 | assert.deepEqual({ "a": 1, 2: "b" }, ObjectFromEntriesPolyfill([["a", 1], [2, "b"]])); 36 | 37 | assert.deepEqual({}, ObjectFromEntriesPolyfill((function* () { })())); 38 | // eslint-disable-next-line @typescript-eslint/naming-convention 39 | const entries = (function* () { yield* [["a", 1], [2, "b"]]; })() as unknown as Iterable; 40 | assert.deepEqual({ "a": 1, 2: "b" }, ObjectFromEntriesPolyfill(entries)); 41 | 42 | assert.throws(() => ObjectFromEntriesPolyfill(1 as any)); 43 | }); 44 | 45 | it("WeakRef API polyfill should work", () => { 46 | const weakRefCtor = getWeakRefStub(); 47 | 48 | const obj: {} | null = {}; 49 | const objWeakRef = new weakRefCtor(obj); 50 | 51 | assert.strictEqual(objWeakRef.deref(), obj); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/ProjectConfigTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { Config, ProjectConfig } from "../src/ProjectConfig"; 4 | 5 | describe("ProjectConfig", () => { 6 | it("isEmpty - empty instance should be empty", () => { 7 | assert.isTrue(ProjectConfig.empty.isEmpty); 8 | }); 9 | 10 | it("isEmpty - empty instance with increased timestamp should be empty", () => { 11 | assert.isTrue(ProjectConfig.empty.with(1000).isEmpty); 12 | }); 13 | 14 | it("isEmpty - instance with config should not be empty", () => { 15 | assert.isFalse(new ProjectConfig("{}", new Config({}), 1000, "\"eTag\"").isEmpty); 16 | }); 17 | 18 | it("with - returned instance should contain the same data except for timestamp - empty", () => { 19 | const timestamp = 1000; 20 | const returnedPc = ProjectConfig.empty.with(timestamp); 21 | assert.isUndefined(returnedPc.config); 22 | assert.isUndefined(returnedPc.configJson); 23 | assert.isUndefined(returnedPc.httpETag); 24 | assert.equal(returnedPc.timestamp, timestamp); 25 | }); 26 | 27 | it("with - returned instance should contain the same data except for timestamp - non-empty", () => { 28 | const timestamp = 2000; 29 | const pc = new ProjectConfig("{}", new Config({}), 1000, "\"eTag\""); 30 | const returnedPc = pc.with(timestamp); 31 | assert.equal(returnedPc.config, pc.config); 32 | assert.equal(returnedPc.configJson, pc.configJson); 33 | assert.equal(returnedPc.httpETag, pc.httpETag); 34 | assert.equal(returnedPc.timestamp, timestamp); 35 | }); 36 | 37 | it("isExpired - empty instance is expired", () => { 38 | assert.isTrue(ProjectConfig.empty.isExpired(1000)); 39 | }); 40 | 41 | it("isExpired - empty instance with increased timestamp is expired when timestamp is too old", () => { 42 | const timestamp = ProjectConfig.generateTimestamp() - 2000; 43 | assert.isTrue(ProjectConfig.empty.with(timestamp).isExpired(1000)); 44 | }); 45 | 46 | it("isExpired - empty instance with increased timestamp is not expired when timestamp is not old enough", () => { 47 | const timestamp = ProjectConfig.generateTimestamp(); 48 | assert.isFalse(ProjectConfig.empty.with(timestamp).isExpired(1000)); 49 | }); 50 | 51 | it("isExpired - instance with config is expired when timestamp is too old", () => { 52 | const timestamp = ProjectConfig.generateTimestamp() - 2000; 53 | assert.isTrue(new ProjectConfig("{}", new Config({}), timestamp, "\"eTag\"").isExpired(1000)); 54 | }); 55 | 56 | it("isExpired - instance with config is not expired when timestamp is not old enough", () => { 57 | const timestamp = ProjectConfig.generateTimestamp(); 58 | assert.isFalse(new ProjectConfig("{}", new Config({}), timestamp, "\"eTag\"").isExpired(1000)); 59 | }); 60 | 61 | it("serialization works - empty", () => { 62 | const pc = ProjectConfig.empty; 63 | 64 | const serializedPc = ProjectConfig.serialize(pc); 65 | const deserializedPc = ProjectConfig.deserialize(serializedPc); 66 | 67 | assert.isUndefined(deserializedPc.config); 68 | assert.isUndefined(deserializedPc.configJson); 69 | assert.isUndefined(deserializedPc.httpETag); 70 | assert.equal(deserializedPc.timestamp, pc.timestamp); 71 | assert.isTrue(deserializedPc.isEmpty); 72 | }); 73 | 74 | it("serialization works - non-empty", () => { 75 | const pc = new ProjectConfig("{}", new Config({}), 1000, "\"eTag\""); 76 | 77 | const serializedPc = ProjectConfig.serialize(pc); 78 | const deserializedPc = ProjectConfig.deserialize(serializedPc); 79 | 80 | assert.isObject(deserializedPc.config); 81 | assert.equal(deserializedPc.configJson, pc.configJson); 82 | assert.equal(deserializedPc.httpETag, pc.httpETag); 83 | assert.equal(deserializedPc.timestamp, pc.timestamp); 84 | assert.isFalse(deserializedPc.isEmpty); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/UserTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { User } from "../src/index"; 4 | import { WellKnownUserObjectAttribute, getUserAttribute, getUserAttributes } from "../src/User"; 5 | 6 | const identifierAttribute: WellKnownUserObjectAttribute = "Identifier"; 7 | const emailAttribute: WellKnownUserObjectAttribute = "Email"; 8 | const countryAttribute: WellKnownUserObjectAttribute = "Country"; 9 | 10 | function createUser(attributeName: string, attributeValue: string) { 11 | const user = new User(""); 12 | switch (attributeName) { 13 | case identifierAttribute: 14 | user.identifier = attributeValue; 15 | break; 16 | case emailAttribute: 17 | user.email = attributeValue; 18 | break; 19 | case countryAttribute: 20 | user.country = attributeValue; 21 | break; 22 | default: 23 | user.custom[attributeName] = attributeValue; 24 | } 25 | return user; 26 | } 27 | 28 | describe("User Object", () => { 29 | 30 | for (const [attributeName, attributeValue, expectedValue] of <[string, string, string][]>[ 31 | [identifierAttribute, void 0, ""], 32 | [identifierAttribute, null, ""], 33 | [identifierAttribute, "", ""], 34 | [identifierAttribute, "id", "id"], 35 | [identifierAttribute, "\t", "\t"], 36 | [identifierAttribute, "\u1F600", "\u1F600"], 37 | [emailAttribute, void 0, void 0], 38 | [emailAttribute, null, void 0], 39 | [emailAttribute, "", ""], 40 | [emailAttribute, "a@example.com", "a@example.com"], 41 | [countryAttribute, void 0, void 0], 42 | [countryAttribute, null, void 0], 43 | [countryAttribute, "", ""], 44 | [countryAttribute, "US", "US"], 45 | ["Custom1", void 0, void 0], 46 | ["Custom1", null, void 0], 47 | ["Custom1", "", ""], 48 | ["Custom1", "3.14", "3.14"], 49 | ]) { 50 | it(`Create user - should set attribute value - ${attributeName}: ${attributeValue}`, () => { 51 | const user = createUser(attributeName, attributeValue); 52 | 53 | assert.strictEqual(getUserAttributes(user)[attributeName], expectedValue); 54 | assert.strictEqual(getUserAttribute(user, attributeName) ?? void 0, expectedValue); 55 | }); 56 | } 57 | 58 | it("Create User with id, email and country - all attributes should contain passed values", () => { 59 | // Arrange 60 | 61 | const user = new User("id", "id@example.com", "US"); 62 | 63 | // Act 64 | 65 | const actualAttributes = getUserAttributes(user); 66 | 67 | // Assert 68 | 69 | assert.strictEqual(actualAttributes[identifierAttribute], "id"); 70 | assert.strictEqual(getUserAttribute(user, identifierAttribute), "id"); 71 | 72 | assert.strictEqual(actualAttributes[emailAttribute], "id@example.com"); 73 | assert.strictEqual(getUserAttribute(user, emailAttribute), "id@example.com"); 74 | 75 | assert.strictEqual(actualAttributes[countryAttribute], "US"); 76 | assert.strictEqual(getUserAttribute(user, countryAttribute), "US"); 77 | 78 | assert.equal(3, Object.keys(actualAttributes).length); 79 | }); 80 | 81 | it("Use well-known attributes as custom properties - should not append all attributes", () => { 82 | // Arrange 83 | 84 | const user = new User("id", "id@example.com", "US", { 85 | myCustomAttribute: "myCustomAttributeValue", 86 | [identifierAttribute]: "myIdentifier", 87 | [countryAttribute]: "United States", 88 | [emailAttribute]: "otherEmail@example.com" 89 | }); 90 | 91 | // Act 92 | 93 | const actualAttributes = getUserAttributes(user); 94 | 95 | // Assert 96 | 97 | assert.strictEqual(actualAttributes[identifierAttribute], "id"); 98 | assert.strictEqual(getUserAttribute(user, identifierAttribute), "id"); 99 | 100 | assert.strictEqual(actualAttributes[emailAttribute], "id@example.com"); 101 | assert.strictEqual(getUserAttribute(user, emailAttribute), "id@example.com"); 102 | 103 | assert.strictEqual(actualAttributes[countryAttribute], "US"); 104 | assert.strictEqual(getUserAttribute(user, countryAttribute), "US"); 105 | 106 | assert.strictEqual(actualAttributes["myCustomAttribute"], "myCustomAttributeValue"); 107 | assert.strictEqual(getUserAttribute(user, "myCustomAttribute"), "myCustomAttributeValue"); 108 | 109 | assert.equal(4, Object.keys(actualAttributes).length); 110 | }); 111 | 112 | for (const [attributeName, attributeValue] of <[string, string][]>[ 113 | ["identifier", "myId"], 114 | ["IDENTIFIER", "myId"], 115 | ["email", "theBoss@example.com"], 116 | ["EMAIL", "theBoss@example.com"], 117 | ["eMail", "theBoss@example.com"], 118 | ["country", "myHome"], 119 | ["COUNTRY", "myHome"], 120 | ]) { 121 | it(`Use well-known attributes as custom properties with different casings - should append all attributes - attributeName: ${attributeName} | attributeValue: ${attributeValue}`, () => { 122 | // Arrange 123 | 124 | const user = new User("id", "id@example.com", "US", { 125 | [attributeName]: attributeValue, 126 | }); 127 | 128 | // Act 129 | 130 | const actualAttributes = getUserAttributes(user); 131 | 132 | // Assert 133 | 134 | assert.strictEqual(actualAttributes[attributeName], attributeValue); 135 | assert.strictEqual(getUserAttribute(user, attributeName), attributeValue); 136 | 137 | assert.equal(4, Object.keys(actualAttributes).length); 138 | }); 139 | } 140 | 141 | it("Non-standard instantiation should work", () => { 142 | // Arrange 143 | 144 | const user = { 145 | identifier: "id", 146 | email: "id@example.com", 147 | country: "US", 148 | custom: { 149 | myCustomAttribute: "myCustomAttributeValue" 150 | } 151 | }; 152 | 153 | // Act 154 | 155 | const actualAttributes = getUserAttributes(user); 156 | 157 | // Assert 158 | 159 | assert.strictEqual(actualAttributes[identifierAttribute], "id"); 160 | assert.strictEqual(getUserAttribute(user, identifierAttribute), "id"); 161 | 162 | assert.strictEqual(actualAttributes[emailAttribute], "id@example.com"); 163 | assert.strictEqual(getUserAttribute(user, emailAttribute), "id@example.com"); 164 | 165 | assert.strictEqual(actualAttributes[countryAttribute], "US"); 166 | assert.strictEqual(getUserAttribute(user, countryAttribute), "US"); 167 | 168 | assert.strictEqual(actualAttributes["myCustomAttribute"], "myCustomAttributeValue"); 169 | assert.strictEqual(getUserAttribute(user, "myCustomAttribute"), "myCustomAttributeValue"); 170 | 171 | assert.equal(4, Object.keys(actualAttributes).length); 172 | }); 173 | 174 | }); 175 | -------------------------------------------------------------------------------- /test/VariationIdTests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import "mocha"; 3 | import { ConfigCatClient, IConfigCatClient } from "../src/ConfigCatClient"; 4 | import { AutoPollOptions } from "../src/ConfigCatClientOptions"; 5 | import { FakeConfigCatKernel, FakeConfigFetcherWithNullNewConfig, FakeConfigFetcherWithPercentageOptionsWithinTargetingRule, FakeConfigFetcherWithTwoKeysAndRules } from "./helpers/fakes"; 6 | 7 | describe("ConfigCatClient", () => { 8 | it("getKeyAndValueAsync() works with default", async () => { 9 | const configCatKernel: FakeConfigCatKernel = { 10 | configFetcher: new FakeConfigFetcherWithTwoKeysAndRules(), 11 | sdkType: "common", 12 | sdkVersion: "1.0.0" 13 | }; 14 | const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { logger: null }, null); 15 | const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); 16 | 17 | const result = await client.getKeyAndValueAsync("abcdefgh"); 18 | assert.equal(result?.settingKey, "debug"); 19 | assert.equal(result?.settingValue, "def"); 20 | }); 21 | 22 | it("getKeyAndValueAsync() works with rollout rules", async () => { 23 | const configCatKernel: FakeConfigCatKernel = { 24 | configFetcher: new FakeConfigFetcherWithTwoKeysAndRules(), 25 | sdkType: "common", 26 | sdkVersion: "1.0.0" 27 | }; 28 | const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { logger: null }, null); 29 | const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); 30 | 31 | const result = await client.getKeyAndValueAsync("6ada5ff2"); 32 | assert.equal(result?.settingKey, "debug"); 33 | assert.equal(result?.settingValue, "value"); 34 | }); 35 | 36 | it("getKeyAndValueAsync() works with percentage options", async () => { 37 | const configCatKernel: FakeConfigCatKernel = { 38 | configFetcher: new FakeConfigFetcherWithTwoKeysAndRules(), 39 | sdkType: "common", 40 | sdkVersion: "1.0.0" 41 | }; 42 | const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { logger: null }, null); 43 | const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); 44 | 45 | const result = await client.getKeyAndValueAsync("622f5d07"); 46 | assert.equal(result?.settingKey, "debug2"); 47 | assert.equal(result?.settingValue, "value2"); 48 | }); 49 | 50 | it("getKeyAndValueAsync() works with percentage options within targeting rule", async () => { 51 | const configCatKernel: FakeConfigCatKernel = { 52 | configFetcher: new FakeConfigFetcherWithPercentageOptionsWithinTargetingRule(), 53 | sdkType: "common", 54 | sdkVersion: "1.0.0" 55 | }; 56 | const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { logger: null }, null); 57 | const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); 58 | 59 | const result = await client.getKeyAndValueAsync("622f5d07"); 60 | assert.equal(result?.settingKey, "debug"); 61 | assert.equal(result?.settingValue, "value2"); 62 | }); 63 | 64 | it("getKeyAndValueAsync() with null config", async () => { 65 | const configCatKernel: FakeConfigCatKernel = { 66 | configFetcher: new FakeConfigFetcherWithNullNewConfig(), 67 | sdkType: "common", 68 | sdkVersion: "1.0.0" 69 | }; 70 | const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { logger: null, maxInitWaitTimeSeconds: 0 }, null); 71 | const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); 72 | 73 | const result = await client.getKeyAndValueAsync("622f5d07"); 74 | assert.isNull(result); 75 | }); 76 | 77 | it("getKeyAndValueAsync() with non-existing id", async () => { 78 | const configCatKernel: FakeConfigCatKernel = { 79 | configFetcher: new FakeConfigFetcherWithTwoKeysAndRules(), 80 | sdkType: "common", 81 | sdkVersion: "1.0.0" 82 | }; 83 | const options: AutoPollOptions = new AutoPollOptions("APIKEY", "common", "1.0.0", { logger: null }, null); 84 | const client: IConfigCatClient = new ConfigCatClient(options, configCatKernel); 85 | 86 | const result = await client.getKeyAndValueAsync("non-exisiting"); 87 | assert.isNull(result); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/data/evaluationlog/1_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "stringContainsDogDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "1_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "stringContainsDogDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "1_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringContainsDogDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "1_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringContainsDogDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": "Dog", 38 | "expectedLog": "1_rule_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/data/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule 4 | Returning 'Dog'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /test/data/evaluationlog/1_targeting_rule/1_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /test/data/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match 4 | Returning 'Cat'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/2_targeting_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "stringIsInDogDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "2_rules_no_user.txt" 10 | }, 11 | { 12 | "key": "stringIsInDogDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "2_rules_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringIsInDogDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Custom1": "user" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "2_rules_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringIsInDogDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Custom1": "admin" 36 | }, 37 | "returnValue": "Dog", 38 | "expectedLog": "2_rules_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/data/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule 7 | Returning 'Dog'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' 4 | Evaluating targeting rules and applying the first match if any: 5 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing 8 | The current targeting rule is ignored and the evaluation continues with the next rule. 9 | Returning 'Cat'. 10 | -------------------------------------------------------------------------------- /test/data/evaluationlog/2_targeting_rules/2_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing 7 | The current targeting rule is ignored and the evaluation continues with the next rule. 8 | Returning 'Cat'. 9 | -------------------------------------------------------------------------------- /test/data/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/_overrides/test_list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0, 5 | "s": "test-salt" 6 | }, 7 | "f": { 8 | "booleanKey1": { 9 | "t": 0, 10 | "v": { 11 | "b": false 12 | }, 13 | "r": [ 14 | { 15 | "c": [ 16 | { 17 | "u": { 18 | "a": "Identifier", 19 | "c": 2, 20 | "l": [ 21 | "1", 22 | "2", 23 | "3", 24 | "4", 25 | "5", 26 | "6", 27 | "7", 28 | "8", 29 | "9", 30 | "10" 31 | ] 32 | } 33 | }, 34 | { 35 | "u": { 36 | "a": "Identifier", 37 | "c": 2, 38 | "l": [ 39 | "1", 40 | "2", 41 | "3", 42 | "4", 43 | "5", 44 | "6", 45 | "7", 46 | "8", 47 | "9", 48 | "10", 49 | "11" 50 | ] 51 | } 52 | }, 53 | { 54 | "u": { 55 | "a": "Identifier", 56 | "c": 2, 57 | "l": [ 58 | "1", 59 | "2", 60 | "3", 61 | "4", 62 | "5", 63 | "6", 64 | "7", 65 | "8", 66 | "9", 67 | "10", 68 | "11", 69 | "12" 70 | ] 71 | } 72 | } 73 | ], 74 | "s": { 75 | "v": { 76 | "b": true 77 | } 78 | } 79 | } 80 | ] 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/data/evaluationlog/and_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 4 | "tests": [ 5 | { 6 | "key": "emailAnd", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "and_rules_no_user.txt" 10 | }, 11 | { 12 | "key": "emailAnd", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345", 16 | "Email": "jane@configcat.com" 17 | }, 18 | "returnValue": "Cat", 19 | "expectedLog": "and_rules_user.txt" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/data/evaluationlog/and_rules/and_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'emailAnd' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 5 | THEN 'Dog' => cannot evaluate, User Object is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/and_rules/and_rules_user.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true 4 | AND User.Email CONTAINS ANY OF ['@'] => true 5 | AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 6 | THEN 'Dog' => no match 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/comparators.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 4 | "tests": [ 5 | { 6 | "key": "allinone", 7 | "defaultValue": "", 8 | "user": { 9 | "Identifier": "12345", 10 | "Email": "joe@example.com", 11 | "Country": "[\"USA\"]", 12 | "Version": "1.0.0", 13 | "Number": "1.0", 14 | "Date": "1693497500" 15 | }, 16 | "returnValue": "default", 17 | "expectedLog": "allinone.txt" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/data/evaluationlog/comparators/allinone.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email EQUALS '' => true 4 | AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions 5 | THEN '1h' => no match 6 | - IF User.Email EQUALS 'joe@example.com' => true 7 | AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions 8 | THEN '1c' => no match 9 | - IF User.Email IS ONE OF [<1 hashed value>] => true 10 | AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 11 | THEN '2h' => no match 12 | - IF User.Email IS ONE OF ['joe@example.com'] => true 13 | AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions 14 | THEN '2c' => no match 15 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true 16 | AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 17 | THEN '3h' => no match 18 | - IF User.Email STARTS WITH ANY OF ['joe@'] => true 19 | AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions 20 | THEN '3c' => no match 21 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true 22 | AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 23 | THEN '4h' => no match 24 | - IF User.Email ENDS WITH ANY OF ['@example.com'] => true 25 | AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions 26 | THEN '4c' => no match 27 | - IF User.Email CONTAINS ANY OF ['e@e'] => true 28 | AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions 29 | THEN '5' => no match 30 | - IF User.Version IS ONE OF ['1.0.0'] => true 31 | AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions 32 | THEN '6' => no match 33 | - IF User.Version < '1.0.1' => true 34 | AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions 35 | THEN '7' => no match 36 | - IF User.Version > '0.9.9' => true 37 | AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions 38 | THEN '8' => no match 39 | - IF User.Number = '1' => true 40 | AND User.Number != '1' => false, skipping the remaining AND conditions 41 | THEN '9' => no match 42 | - IF User.Number < '1.1' => true 43 | AND User.Number >= '1.1' => false, skipping the remaining AND conditions 44 | THEN '10' => no match 45 | - IF User.Number > '0.9' => true 46 | AND User.Number <= '0.9' => false, skipping the remaining AND conditions 47 | THEN '11' => no match 48 | - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true 49 | AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions 50 | THEN '12' => no match 51 | - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true 52 | AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 53 | THEN '13h' => no match 54 | - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true 55 | AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions 56 | THEN '13c' => no match 57 | Returning 'default'. 58 | -------------------------------------------------------------------------------- /test/data/evaluationlog/epoch_date_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 4 | "tests": [ 5 | { 6 | "key": "boolTrueIn202304", 7 | "defaultValue": true, 8 | "returnValue": false, 9 | "expectedLog": "date_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "2023.04.10" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/data/evaluationlog/epoch_date_validation/date_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions 5 | THEN 'true' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'false'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonOverride": "test_list_truncation.json", 3 | "tests": [ 4 | { 5 | "key": "booleanKey1", 6 | "defaultValue": false, 7 | "user": { 8 | "Identifier": "12" 9 | }, 10 | "returnValue": true, 11 | "expectedLog": "list_truncation.txt" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/data/evaluationlog/list_truncation/list_truncation.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true 4 | AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true 5 | AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true 6 | THEN 'true' => MATCH, applying rule 7 | Returning 'true'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/number_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", 4 | "tests": [ 5 | { 6 | "key": "number", 7 | "defaultValue": "default", 8 | "returnValue": "Default", 9 | "expectedLog": "number_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "not_a_number" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/data/evaluationlog/number_validation/number_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Default'. 7 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_after_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "integer25One25Two25Three25FourAdvancedRules", 7 | "defaultValue": 42, 8 | "returnValue": -1, 9 | "expectedLog": "options_after_targeting_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "integer25One25Two25Three25FourAdvancedRules", 13 | "defaultValue": 42, 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": 2, 18 | "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "integer25One25Two25Three25FourAdvancedRules", 22 | "defaultValue": 42, 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": 2, 28 | "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "integer25One25Two25Three25FourAdvancedRules", 32 | "defaultValue": 42, 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": 5, 38 | "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule 4 | Returning '5'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Evaluating % options based on the User.Identifier attribute: 7 | - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) 8 | - Hash value 25 selects % option 2 (25%), '2'. 9 | Returning '2'. 10 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Skipping % options because the User Object is missing. 7 | Returning '-1'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match 4 | Evaluating % options based on the User.Identifier attribute: 5 | - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) 6 | - Hash value 25 selects % option 2 (25%), '2'. 7 | Returning '2'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_custom_attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 4 | "tests": [ 5 | { 6 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "options_custom_attribute_no_user.txt" 10 | }, 11 | { 12 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Chicken", 18 | "expectedLog": "no_options_custom_attribute.txt" 19 | }, 20 | { 21 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Country": "US" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "matching_options_custom_attribute.txt" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' 2 | Evaluating % options based on the User.Country attribute: 3 | - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) 4 | - Hash value 70 selects % option 1 (75%), 'Cat'. 5 | Returning 'Cat'. 6 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' 3 | Skipping % options because the User.Country attribute is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' 3 | Skipping % options because the User Object is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_user_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "string75Cat0Dog25Falcon0Horse", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "options_user_attribute_no_user.txt" 10 | }, 11 | { 12 | "key": "string75Cat0Dog25Falcon0Horse", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "options_user_attribute_user.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' 3 | Skipping % options because the User Object is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' 2 | Evaluating % options based on the User.Identifier attribute: 3 | - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) 4 | - Hash value 21 selects % option 1 (75%), 'Cat'. 5 | Returning 'Cat'. 6 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_within_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 4 | "tests": [ 5 | { 6 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "options_within_targeting_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": "Cat", 38 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" 39 | }, 40 | { 41 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 42 | "defaultValue": "default", 43 | "user": { 44 | "Identifier": "12345", 45 | "Email": "joe@configcat.com", 46 | "Country": "US" 47 | }, 48 | "returnValue": "Cat", 49 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule 5 | Skipping % options because the User.Country attribute is missing. 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule 4 | Evaluating % options based on the User.Country attribute: 5 | - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) 6 | - Hash value 63 selects % option 1 (75%), 'Cat'. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /test/data/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match 4 | Returning 'Cat'. 5 | -------------------------------------------------------------------------------- /test/data/evaluationlog/prerequisite_flag.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 4 | "tests": [ 5 | { 6 | "key": "dependentFeatureWithUserCondition", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" 10 | }, 11 | { 12 | "key": "dependentFeature", 13 | "defaultValue": "default", 14 | "returnValue": "Chicken", 15 | "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" 16 | }, 17 | { 18 | "key": "dependentFeatureWithUserCondition2", 19 | "defaultValue": "default", 20 | "returnValue": "Frog", 21 | "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" 22 | }, 23 | { 24 | "key": "dependentFeature", 25 | "defaultValue": "default", 26 | "user": { 27 | "Identifier": "12345", 28 | "Email": "kate@configcat.com", 29 | "Country": "USA" 30 | }, 31 | "returnValue": "Horse", 32 | "expectedLog": "prerequisite_flag.txt" 33 | }, 34 | { 35 | "key": "dependentFeatureMultipleLevels", 36 | "defaultValue": "default", 37 | "returnValue": "Dog", 38 | "expectedLog": "prerequisite_flag_multilevel.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/data/evaluationlog/prerequisite_flag/prerequisite_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF Flag 'mainFeature' EQUALS 'target' 4 | ( 5 | Evaluating prerequisite flag 'mainFeature': 6 | Evaluating targeting rules and applying the first match if any: 7 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 8 | THEN 'private' => no match 9 | - IF User.Country IS ONE OF [<1 hashed value>] => true 10 | AND User IS NOT IN SEGMENT 'Beta Users' 11 | ( 12 | Evaluating segment 'Beta Users': 13 | - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions 14 | Segment evaluation result: User IS NOT IN SEGMENT. 15 | Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. 16 | ) => true 17 | AND User IS NOT IN SEGMENT 'Developers' 18 | ( 19 | Evaluating segment 'Developers': 20 | - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions 21 | Segment evaluation result: User IS NOT IN SEGMENT. 22 | Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. 23 | ) => true 24 | THEN 'target' => MATCH, applying rule 25 | Prerequisite flag evaluation result: 'target'. 26 | Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. 27 | ) 28 | THEN % options => MATCH, applying rule 29 | Evaluating % options based on the User.Identifier attribute: 30 | - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) 31 | - Hash value 78 selects % option 4 (25%), 'Horse'. 32 | Returning 'Horse'. 33 | -------------------------------------------------------------------------------- /test/data/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'dependentFeatureMultipleLevels' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF Flag 'intermediateFeature' EQUALS 'true' 4 | ( 5 | Evaluating prerequisite flag 'intermediateFeature': 6 | Evaluating targeting rules and applying the first match if any: 7 | - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' 8 | ( 9 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 10 | Prerequisite flag evaluation result: 'true'. 11 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. 12 | ) => true 13 | AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' 14 | ( 15 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 16 | Prerequisite flag evaluation result: 'true'. 17 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. 18 | ) => true 19 | THEN 'true' => MATCH, applying rule 20 | Prerequisite flag evaluation result: 'true'. 21 | Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. 22 | ) 23 | THEN 'Dog' => MATCH, applying rule 24 | Returning 'Dog'. 25 | -------------------------------------------------------------------------------- /test/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 4 | INFO [5000] Evaluating 'dependentFeatureWithUserCondition2' 5 | Evaluating targeting rules and applying the first match if any: 6 | - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing 7 | The current targeting rule is ignored and the evaluation continues with the next rule. 8 | - IF Flag 'mainFeature' EQUALS 'public' 9 | ( 10 | Evaluating prerequisite flag 'mainFeature': 11 | Evaluating targeting rules and applying the first match if any: 12 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 13 | THEN 'private' => cannot evaluate, User Object is missing 14 | The current targeting rule is ignored and the evaluation continues with the next rule. 15 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 16 | THEN 'target' => cannot evaluate, User Object is missing 17 | The current targeting rule is ignored and the evaluation continues with the next rule. 18 | Prerequisite flag evaluation result: 'public'. 19 | Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. 20 | ) 21 | THEN % options => MATCH, applying rule 22 | Skipping % options because the User Object is missing. 23 | The current targeting rule is ignored and the evaluation continues with the next rule. 24 | - IF Flag 'mainFeature' EQUALS 'public' 25 | ( 26 | Evaluating prerequisite flag 'mainFeature': 27 | Evaluating targeting rules and applying the first match if any: 28 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 29 | THEN 'private' => cannot evaluate, User Object is missing 30 | The current targeting rule is ignored and the evaluation continues with the next rule. 31 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 32 | THEN 'target' => cannot evaluate, User Object is missing 33 | The current targeting rule is ignored and the evaluation continues with the next rule. 34 | Prerequisite flag evaluation result: 'public'. 35 | Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. 36 | ) 37 | THEN 'Frog' => MATCH, applying rule 38 | Returning 'Frog'. 39 | -------------------------------------------------------------------------------- /test/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'dependentFeatureWithUserCondition' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' 7 | ( 8 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 9 | Prerequisite flag evaluation result: 'true'. 10 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. 11 | ) 12 | THEN % options => MATCH, applying rule 13 | Skipping % options because the User Object is missing. 14 | The current targeting rule is ignored and the evaluation continues with the next rule. 15 | Returning 'Chicken'. 16 | -------------------------------------------------------------------------------- /test/data/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'dependentFeature' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF Flag 'mainFeature' EQUALS 'target' 5 | ( 6 | Evaluating prerequisite flag 'mainFeature': 7 | Evaluating targeting rules and applying the first match if any: 8 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 9 | THEN 'private' => cannot evaluate, User Object is missing 10 | The current targeting rule is ignored and the evaluation continues with the next rule. 11 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 12 | THEN 'target' => cannot evaluate, User Object is missing 13 | The current targeting rule is ignored and the evaluation continues with the next rule. 14 | Prerequisite flag evaluation result: 'public'. 15 | Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. 16 | ) 17 | THEN % options => no match 18 | Returning 'Chicken'. 19 | -------------------------------------------------------------------------------- /test/data/evaluationlog/segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", 4 | "tests": [ 5 | { 6 | "key": "featureWithSegmentTargeting", 7 | "defaultValue": false, 8 | "returnValue": false, 9 | "expectedLog": "segment_no_user.txt" 10 | }, 11 | { 12 | "key": "featureWithSegmentTargetingMultipleConditions", 13 | "defaultValue": false, 14 | "returnValue": false, 15 | "expectedLog": "segment_no_user_multi_conditions.txt" 16 | }, 17 | { 18 | "key": "featureWithNegatedSegmentTargetingCleartext", 19 | "defaultValue": false, 20 | "user": { 21 | "Identifier": "12345" 22 | }, 23 | "returnValue": false, 24 | "expectedLog": "segment_no_targeted_attribute.txt" 25 | }, 26 | { 27 | "key": "featureWithSegmentTargeting", 28 | "defaultValue": false, 29 | "user": { 30 | "Identifier": "12345", 31 | "Email": "jane@example.com" 32 | }, 33 | "returnValue": true, 34 | "expectedLog": "segment_matching.txt" 35 | }, 36 | { 37 | "key": "featureWithNegatedSegmentTargeting", 38 | "defaultValue": false, 39 | "user": { 40 | "Identifier": "12345", 41 | "Email": "jane@example.com" 42 | }, 43 | "returnValue": false, 44 | "expectedLog": "segment_no_matching.txt" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /test/data/evaluationlog/segment/segment_matching.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User IS IN SEGMENT 'Beta users' 4 | ( 5 | Evaluating segment 'Beta users': 6 | - IF User.Email IS ONE OF [<2 hashed values>] => true 7 | Segment evaluation result: User IS IN SEGMENT. 8 | Condition (User IS IN SEGMENT 'Beta users') evaluates to true. 9 | ) 10 | THEN 'true' => MATCH, applying rule 11 | Returning 'true'. 12 | -------------------------------------------------------------------------------- /test/data/evaluationlog/segment/segment_no_matching.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User IS NOT IN SEGMENT 'Beta users' 4 | ( 5 | Evaluating segment 'Beta users': 6 | - IF User.Email IS ONE OF [<2 hashed values>] => true 7 | Segment evaluation result: User IS IN SEGMENT. 8 | Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. 9 | ) 10 | THEN 'true' => no match 11 | Returning 'false'. 12 | -------------------------------------------------------------------------------- /test/data/evaluationlog/segment/segment_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' 5 | ( 6 | Evaluating segment 'Beta users (cleartext)': 7 | - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions 8 | Segment evaluation result: cannot evaluate, the User.Email attribute is missing. 9 | Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. 10 | ) 11 | THEN 'true' => cannot evaluate, the User.Email attribute is missing 12 | The current targeting rule is ignored and the evaluation continues with the next rule. 13 | Returning 'false'. 14 | -------------------------------------------------------------------------------- /test/data/evaluationlog/segment/segment_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithSegmentTargeting' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS IN SEGMENT 'Beta users' THEN 'true' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'false'. 7 | -------------------------------------------------------------------------------- /test/data/evaluationlog/segment/segment_no_user_multi_conditions.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `getValueAsync()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions 5 | THEN 'true' => cannot evaluate, User Object is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'false'. 8 | -------------------------------------------------------------------------------- /test/data/evaluationlog/semver_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", 4 | "tests": [ 5 | { 6 | "key": "isNotOneOf", 7 | "defaultValue": "default", 8 | "returnValue": "Default", 9 | "expectedLog": "semver_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "wrong_semver" 13 | } 14 | }, 15 | { 16 | "key": "relations", 17 | "defaultValue": "default", 18 | "returnValue": "Default", 19 | "expectedLog": "semver_relations_error.txt", 20 | "user": { 21 | "Identifier": "12345", 22 | "Custom1": "wrong_semver" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/data/evaluationlog/semver_validation/semver_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 3 | INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' 4 | Evaluating targeting rules and applying the first match if any: 5 | - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 8 | The current targeting rule is ignored and the evaluation continues with the next rule. 9 | Returning 'Default'. 10 | -------------------------------------------------------------------------------- /test/data/evaluationlog/semver_validation/semver_relations_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 3 | WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 4 | WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 5 | WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 6 | INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' 7 | Evaluating targeting rules and applying the first match if any: 8 | - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 9 | The current targeting rule is ignored and the evaluation continues with the next rule. 10 | - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 11 | The current targeting rule is ignored and the evaluation continues with the next rule. 12 | - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 13 | The current targeting rule is ignored and the evaluation continues with the next rule. 14 | - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 15 | The current targeting rule is ignored and the evaluation continues with the next rule. 16 | - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 17 | The current targeting rule is ignored and the evaluation continues with the next rule. 18 | Returning 'Default'. 19 | -------------------------------------------------------------------------------- /test/data/evaluationlog/simple_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "boolDefaultFalse", 7 | "defaultValue": true, 8 | "returnValue": false, 9 | "expectedLog": "off_flag.txt" 10 | }, 11 | { 12 | "key": "boolDefaultTrue", 13 | "defaultValue": false, 14 | "returnValue": true, 15 | "expectedLog": "on_flag.txt" 16 | }, 17 | { 18 | "key": "stringDefaultCat", 19 | "defaultValue": "Default", 20 | "returnValue": "Cat", 21 | "expectedLog": "text_setting.txt" 22 | }, 23 | { 24 | "key": "integerDefaultOne", 25 | "defaultValue": 0, 26 | "returnValue": 1, 27 | "expectedLog": "int_setting.txt" 28 | }, 29 | { 30 | "testName": "double_setting", 31 | "key": "doubleDefaultPi", 32 | "defaultValue": 0.0, 33 | "returnValue": 3.1415, 34 | "expectedLog": "double_setting.txt" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /test/data/evaluationlog/simple_value/double_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'doubleDefaultPi' 2 | Returning '3.1415'. 3 | -------------------------------------------------------------------------------- /test/data/evaluationlog/simple_value/int_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integerDefaultOne' 2 | Returning '1'. 3 | -------------------------------------------------------------------------------- /test/data/evaluationlog/simple_value/off_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultFalse' 2 | Returning 'false'. 3 | -------------------------------------------------------------------------------- /test/data/evaluationlog/simple_value/on_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultTrue' 2 | Returning 'true'. 3 | -------------------------------------------------------------------------------- /test/data/evaluationlog/simple_value/text_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringDefaultCat' 2 | Returning 'Cat'. 3 | -------------------------------------------------------------------------------- /test/data/test_circulardependency_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0 5 | }, 6 | "f": { 7 | "key1": { 8 | "t": 1, 9 | "v": { "s": "key1-value" }, 10 | "r": [ 11 | { 12 | "c": [ 13 | { 14 | "p": { 15 | "f": "key1", 16 | "c": 0, 17 | "v": { "s": "key1-prereq" } 18 | } 19 | } 20 | ], 21 | "s": { "v": { "s": "key1-prereq" } } 22 | } 23 | ] 24 | }, 25 | "key2": { 26 | "t": 1, 27 | "v": { "s": "key2-value" }, 28 | "r": [ 29 | { 30 | "c": [ 31 | { 32 | "p": { 33 | "f": "key3", 34 | "c": 0, 35 | "v": { "s": "key3-prereq" } 36 | } 37 | } 38 | ], 39 | "s": { "v": { "s": "key2-prereq" } } 40 | } 41 | ] 42 | }, 43 | "key3": { 44 | "t": 1, 45 | "v": { "s": "key3-value" }, 46 | "r": [ 47 | { 48 | "c": [ 49 | { 50 | "p": { 51 | "f": "key2", 52 | "c": 0, 53 | "v": { "s": "key2-prereq" } 54 | } 55 | } 56 | ], 57 | "s": { "v": { "s": "key3-prereq" } } 58 | } 59 | ] 60 | }, 61 | "key4": { 62 | "t": 1, 63 | "v": { "s": "key4-value" }, 64 | "r": [ 65 | { 66 | "c": [ 67 | { 68 | "p": { 69 | "f": "key3", 70 | "c": 0, 71 | "v": { "s": "key3-prereq" } 72 | } 73 | } 74 | ], 75 | "s": { "v": { "s": "key4-prereq" } } 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/data/testmatrix_and_or.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr 2 | ##null##;;;;public;Chicken;Cat;Cat 3 | ;;;;public;Chicken;Cat;Cat 4 | jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane 5 | john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John 6 | a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat 7 | mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark 8 | nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat 9 | stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat 10 | jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane 11 | anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat 12 | jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane 13 | jane;jane;##null##;##null##;public;Chicken;Cat;Cat 14 | @sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat 15 | jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat 16 | -------------------------------------------------------------------------------- /test/data/testmatrix_comparators_v6.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat 2 | ##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat 3 | ;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat 4 | a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 5 | b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 6 | c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 7 | anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 8 | bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat 9 | cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat 10 | reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 11 | writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 12 | reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 13 | writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 14 | admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 15 | user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 16 | reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 17 | writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 18 | reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 19 | writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog 20 | admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 21 | admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 22 | admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog 23 | user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 24 | user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 25 | -------------------------------------------------------------------------------- /test/data/testmatrix_number.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;numberWithPercentage;number 2 | ##null##;;;;Default;Default 3 | id1;;;0;<2.1;<>5 4 | id1;;;0.0;<2.1;<>5 5 | id1;;;0,0;<2.1;<>5 6 | id1;;;0.2;<2.1;<>5 7 | id2;;;0,2;<2.1;<>5 8 | id3;;;1;<2.1;<>5 9 | id4;;;1.0;<2.1;<>5 10 | id5;;;1,0;<2.1;<>5 11 | id6;;;1.5;<2.1;<>5 12 | id7;;;1,5;<2.1;<>5 13 | id8;;;2.1;<=2,1;<>5 14 | id9;;;2,1;<=2,1;<>5 15 | id10;;;3.50;=3.5;<>5 16 | id11;;;3,50;=3.5;<>5 17 | id12;;;5;>=5;Default 18 | id13;;;5.0;>=5;Default 19 | id14;;;5,0;>=5;Default 20 | id13;;;5.76;>5;<>5 21 | id14;;;5,76;>5;<>5 22 | id15;;;4;<>4.2;<>5 23 | id16;;;4.0;<>4.2;<>5 24 | id17;;;4,0;<>4.2;<>5 25 | id18;;;4.2;80%;<>5 26 | id19;;;4,2;20%;<>5 27 | -------------------------------------------------------------------------------- /test/data/testmatrix_prerequisite_flag.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse 2 | ##null##;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True 3 | ;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True 4 | john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;False;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;False;42;3.14;True;True;EmptyOn;EmptyOn;True;False 5 | jane@example.com;jane@example.com;##null##;##null##;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True 6 | -------------------------------------------------------------------------------- /test/data/testmatrix_segments.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment 2 | ##null##;;;;False;False;False;False 3 | ;;;;False;False;False;False 4 | john@example.com;john@example.com;##null##;##null##;False;False;False;False 5 | jane@example.com;jane@example.com;##null##;##null##;False;False;False;False 6 | kate@example.com;kate@example.com;##null##;##null##;True;True;True;True 7 | -------------------------------------------------------------------------------- /test/data/testmatrix_segments_old.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext 2 | ##null##;;;;False;False;False;False;False;False;False;False 3 | ;;;;False;False;False;False;False;False;False;False 4 | john@example.com;john@example.com;##null##;##null##;True;True;False;False;False;False;True;True 5 | jane@example.com;jane@example.com;##null##;##null##;True;True;False;False;False;False;True;True 6 | kate@example.com;kate@example.com;##null##;##null##;False;False;True;True;True;True;False;False 7 | -------------------------------------------------------------------------------- /test/data/testmatrix_semantic.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;isOneOf;isOneOfWithPercentage;isNotOneOf;isNotOneOfWithPercentage;lessThanWithPercentage;relations 2 | ##null##;;;;Default;Default;Default;Default;Default;Default 3 | id1;;;0.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 4 | id1;;;0.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 5 | id1;;;0.2.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 6 | id1;;;1;Default;80%;Default;80%;20%;Default 7 | id2;;;1.0;Default;80%;Default;80%;80%;Default 8 | id3;;;1.0.0;Is one of (1.0.0);is one of (1.0.0);Default;80%;80%;<=1.0.0 9 | id4;;;1.0.0.0;Default;80%;Default;20%;20%;Default 10 | id5;;;1.0.0.0.0;Default;80%;Default;80%;80%;Default 11 | id6;;;1.0.1;Default;80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;Default 12 | id7;;;1.0.11;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 13 | id8;;;1.0.111;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 14 | id9;;;1.0.2;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 15 | id10;;;1.0.3;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 16 | id11;;;1.0.4;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 17 | id12;;;1.0.5;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 18 | id13;;;1.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 19 | id14;;;1.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 20 | id15;;;1.1.2;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 21 | id16;;;1.1.3;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 22 | id17;;;1.1.4;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 23 | id18;;;1.1.5;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 24 | id19;;;1.9.0;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 25 | id20;;;1.9.99;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 26 | id21;;;2.0.0;Default;80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);20%;>=2.0.0 27 | id22;;;2.0.1;Is one of ( , 2.0.1, 2.0.2, );80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;>2.0.0 28 | id23;;;2.0.11;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 29 | id24;;;2.0.2;Is one of ( , 2.0.1, 2.0.2, );80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;>2.0.0 30 | id25;;;2.0.3;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 31 | id26;;;3.0.0;Is one of (3.0.0);80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 32 | id27;;;3.0.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 33 | id28;;;3.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 34 | id28;;;3.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 35 | id29;;;5.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 36 | id30;;;5.99.999;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 37 | -------------------------------------------------------------------------------- /test/data/testmatrix_semantic_2.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;AppVersion;precedenceTests 2 | dontcare;;;1.9.1-1;< 1.9.1-2 3 | dontcare;;;1.9.1-2;< 1.9.1-10 4 | dontcare;;;1.9.1-10;< 1.9.1-10a 5 | dontcare;;;1.9.1-10a;< 1.9.1-1a 6 | dontcare;;;1.9.1-1a;< 1.9.1-alpha 7 | dontcare;;;1.9.1-alpha;< 1.9.99-alpha 8 | dontcare;;;1.9.99-alpha;= 1.9.99-alpha 9 | dontcare;;;1.9.99-alpha+build1;= 1.9.99-alpha 10 | dontcare;;;1.9.99-alpha+build2;= 1.9.99-alpha 11 | dontcare;;;1.9.99-alpha2;< 1.9.99-beta 12 | dontcare;;;1.9.99-beta;< 1.9.99-rc 13 | dontcare;;;1.9.99-rc;< 1.9.99-rc.1 14 | dontcare;;;1.9.99-rc.1;< 1.9.99-rc.2 15 | dontcare;;;1.9.99-rc.2;< 1.9.99-rc.20 16 | dontcare;;;1.9.99-rc.9;< 1.9.99-rc.20 17 | dontcare;;;1.9.99-rc.20;< 1.9.99-rc.20a 18 | dontcare;;;1.9.99-rc.20a;< 1.9.99-rc.2a 19 | dontcare;;;1.9.99-rc.2a;< 1.9.99 20 | dontcare;;;1.9.99;< 1.9.100 21 | dontcare;;;1.9.100;< 1.10.0-alpha 22 | dontcare;;;1.10.0-alpha;<= 1.10.0-alpha 23 | dontcare;;;1.10.0;<= 1.10.0 24 | dontcare;;;1.10.1;<= 1.10.1 25 | dontcare;;;1.10.2;<= 1.10.3 26 | dontcare;;;2.0.0;= 2.0.0 27 | dontcare;;;2.0.0+build3;= 2.0.0 28 | dontcare;;;2.0.0+001;= 2.0.0 29 | dontcare;;;2.0.0+20130313144700;= 2.0.0 30 | dontcare;;;2.0.0+exp.sha.5114f85;= 2.0.0 31 | dontcare;;;3.0.0;= 3.0.0+build3 32 | dontcare;;;4.0.0;= 4.0.0+001 33 | dontcare;;;5.0.0;= 5.0.0+20130313144700 34 | dontcare;;;6.0.0;= 6.0.0+exp.sha.5114f85 35 | dontcare;;;7.0.0-patch+metadata;= 7.0.0-patch 36 | dontcare;;;8.0.0-patch+metadata;= 8.0.0-patch+anothermetadata 37 | dontcare;;;9.0.0-patch;= 9.0.0-patch+metadata 38 | dontcare;;;10.0.0;DEFAULT-FROM-CC-APP 39 | dontcare;;;104.0.0;> 103.0.0 40 | dontcare;;;103.0.0;>= 103.0.0 41 | dontcare;;;102.0.0;>= 101.0.0 42 | dontcare;;;101.0.0;>= 101.0.0 43 | dontcare;;;90.104.0;> 90.103.0 44 | dontcare;;;90.103.0;>= 90.103.0 45 | dontcare;;;90.102.0;>= 90.101.0 46 | dontcare;;;90.101.0;>= 90.101.0 47 | dontcare;;;80.0.104;> 80.0.103 48 | dontcare;;;80.0.103;>= 80.0.103 49 | dontcare;;;80.0.102;>= 80.0.101 50 | dontcare;;;80.0.101;>= 80.0.101 51 | dontcare;;;73.0.0;>= 73.0.0-beta.2 52 | dontcare;;;72.0.0;> 72.0.0-beta.2 53 | dontcare;;;72.0.0-beta.2;> 72.0.0-beta.1 54 | dontcare;;;72.0.0-beta.1;> 72.0.0-beta 55 | dontcare;;;72.0.0-beta;> 72.0.0-alpha 56 | dontcare;;;72.0.0-alpha;> 72.0.0-1a 57 | dontcare;;;72.0.0-1a;> 72.0.0-10a 58 | dontcare;;;72.0.0-10aa;> 72.0.0-10a 59 | dontcare;;;72.0.0-10a;> 72.0.0-2 60 | dontcare;;;72.0.0-2;> 72.0.0-1 61 | dontcare;;;71.0.0+metadata;>= 71.0.0+anothermetadata 62 | dontcare;;;71.0.0-patch3+metadata;>= 71.0.0-patch3+anothermetadata 63 | dontcare;;;71.0.0-patch2+metadata;>= 71.0.0-patch2 64 | dontcare;;;71.0.0-patch1;>= 71.0.0-patch1+metadata 65 | dontcare;;;60.73.0;>= 60.73.0-beta.2 66 | dontcare;;;60.72.0;> 60.72.0-beta.2 67 | dontcare;;;60.72.0-beta.2;> 60.72.0-beta.1 68 | dontcare;;;60.72.0-beta.1;> 60.72.0-beta 69 | dontcare;;;60.72.0-beta;> 60.72.0-alpha 70 | dontcare;;;60.72.0-alpha;> 60.72.0-1a 71 | dontcare;;;60.72.0-1a;> 60.72.0-10a 72 | dontcare;;;60.72.0-10aa;> 60.72.0-10a 73 | dontcare;;;60.72.0-10a;> 60.72.0-2 74 | dontcare;;;60.72.0-2;> 60.72.0-1 75 | dontcare;;;60.71.0+metadata;>= 60.71.0+anothermetadata 76 | dontcare;;;60.71.0-patch3+metadata;>= 60.71.0-patch3+anothermetadata 77 | dontcare;;;60.71.0-patch2+metadata;>= 60.71.0-patch2 78 | dontcare;;;60.71.0-patch1;>= 60.71.0-patch1+metadata 79 | dontcare;;;50.60.73;>= 50.60.73-beta.2 80 | dontcare;;;50.60.72;> 50.60.72-beta.2 81 | dontcare;;;50.60.72-beta.2;> 50.60.72-beta.1 82 | dontcare;;;50.60.72-beta.1;> 50.60.72-beta 83 | dontcare;;;50.60.72-beta;> 50.60.72-alpha 84 | dontcare;;;50.60.72-alpha;> 50.60.72-1a 85 | dontcare;;;50.60.72-1a;> 50.60.72-10a 86 | dontcare;;;50.60.72-10aa;> 50.60.72-10a 87 | dontcare;;;50.60.72-10a;> 50.60.72-2 88 | dontcare;;;50.60.72-2;> 50.60.72-1 89 | dontcare;;;50.60.71+metadata;>= 50.60.71+anothermetadata 90 | dontcare;;;50.60.71-patch3+metadata;>= 50.60.71-patch3+anothermetadata 91 | dontcare;;;50.60.71-patch2+metadata;>= 50.60.71-patch2 92 | dontcare;;;50.60.71-patch1;>= 50.60.71-patch1+metadata 93 | dontcare;;;50.60.71-patch1+anothermetadata;>= 50.60.71-patch1+metadata 94 | dontcare;;;40.0.0-patch;>= 40.0.0-patch 95 | dontcare;;;30.0.0-beta;>= 30.0.0-alpha 96 | -------------------------------------------------------------------------------- /test/data/testmatrix_sensitive.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;isOneOfSensitive;isNotOneOfSensitive 2 | ##null##;;;;ToAll;ToAll 3 | id1;macska@example.com;;;Macska;Kigyo 4 | Kutya;;;;Allat;ToAll 5 | Sas;;;;ToAll;Kigyo 6 | Kutya;macska@example.com;;;Macska;ToAll 7 | id1;;Scotland;;Britt;Kigyo 8 | Macska;;USA;;ToAll;Ireland -------------------------------------------------------------------------------- /test/data/testmatrix_unicode.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext 2 | 1;;;ʄǟռƈʏ ȶɛӼȶ;True;True;False;False;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 3 | 1;;;ʄaռƈʏ ȶɛӼȶ;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 4 | 1;;;ÁRVÍZTŰRŐ tükörfúrógép;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False 5 | 1;;;árvíztűrő tükörfúrógép;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False 6 | 1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False 7 | 1;;;árvíztűrő TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 8 | 1;;;u𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False 9 | ;;;𝖚𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False 10 | ;;;u𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False 11 | ;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 12 | 1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False 13 | 1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False 14 | 1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;False;False;True;True 15 | -------------------------------------------------------------------------------- /test/data/testmatrix_variationid.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;boolean;decimal;text;whole 2 | ##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162; 3 | a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; 4 | b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; 5 | a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9; 6 | b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9; 7 | cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9; 8 | bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; 9 | -------------------------------------------------------------------------------- /test/helpers/ConfigCatClientCacheExtensions.ts: -------------------------------------------------------------------------------- 1 | import { ConfigCatClientCache } from "../../src/ConfigCatClient"; 2 | 3 | declare module "../../src/ConfigCatClient" { 4 | interface ConfigCatClientCache { 5 | getSize(): number; 6 | getAliveCount(): number; 7 | } 8 | } 9 | 10 | ConfigCatClientCache.prototype.getSize = function(this: ConfigCatClientCache) { 11 | return Object.keys(this["instances"]).length; 12 | }; 13 | 14 | ConfigCatClientCache.prototype.getAliveCount = function(this: ConfigCatClientCache) { 15 | return Object.values(this["instances"]).filter(([weakRef]) => !!weakRef.deref()).length; 16 | }; 17 | -------------------------------------------------------------------------------- /test/helpers/ConfigLocation.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as util from "util"; 4 | import { ManualPollOptions } from "../../src/ConfigCatClientOptions"; 5 | import { ManualPollConfigService } from "../../src/ManualPollConfigService"; 6 | import { Config } from "../../src/ProjectConfig"; 7 | import { HttpConfigFetcher } from "./HttpConfigFetcher"; 8 | import { sdkType, sdkVersion } from "./utils"; 9 | 10 | const configCache: { [location: string]: Promise } = {}; 11 | 12 | export abstract class ConfigLocation { 13 | abstract getRealLocation(): string; 14 | 15 | abstract fetchConfigAsync(): Promise; 16 | 17 | fetchConfigCachedAsync(): Promise { 18 | const location = this.getRealLocation(); 19 | let configPromise = configCache[location]; 20 | if (!configPromise) { 21 | configCache[location] = configPromise = this.fetchConfigAsync(); 22 | } 23 | return configPromise; 24 | } 25 | } 26 | 27 | export class CdnConfigLocation extends ConfigLocation { 28 | private $options?: ManualPollOptions; 29 | get options(): ManualPollOptions { 30 | return this.$options ??= new ManualPollOptions(this.sdkKey, sdkType, sdkVersion, { 31 | baseUrl: this.baseUrl ?? "https://cdn-eu.configcat.com" 32 | }); 33 | } 34 | 35 | constructor( 36 | readonly sdkKey: string, 37 | readonly baseUrl?: string, 38 | ) { 39 | super(); 40 | } 41 | 42 | getRealLocation(): string { 43 | const url = this.options.getUrl(); 44 | const index = url.lastIndexOf("?"); 45 | return index >= 0 ? url.slice(0, index) : url; 46 | } 47 | 48 | async fetchConfigAsync(): Promise { 49 | const configFetcher = new HttpConfigFetcher(); 50 | const configService = new ManualPollConfigService(configFetcher, this.options); 51 | 52 | const [fetchResult, projectConfig] = await configService.refreshConfigAsync(); 53 | if (!fetchResult.isSuccess) { 54 | throw new Error("Could not fetch config from CDN: " + fetchResult.errorMessage); 55 | } 56 | return projectConfig.config!; 57 | } 58 | 59 | toString(): string { 60 | return this.sdkKey + (this.baseUrl ? ` (${this.baseUrl})` : ""); 61 | } 62 | } 63 | 64 | export class LocalFileConfigLocation extends ConfigLocation { 65 | filePath: string; 66 | 67 | constructor(...paths: ReadonlyArray) { 68 | super(); 69 | this.filePath = path.join(...paths); 70 | } 71 | 72 | getRealLocation(): string { return this.filePath; } 73 | 74 | async fetchConfigAsync(): Promise { 75 | const configJson = await util.promisify(fs.readFile)(this.filePath, "utf8"); 76 | const parsedObject = JSON.parse(configJson); 77 | return new Config(parsedObject); 78 | } 79 | 80 | toString(): string { 81 | return this.getRealLocation(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/helpers/HttpConfigFetcher.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as https from "https"; 3 | import * as tunnel from "tunnel"; 4 | import { URL } from "url"; 5 | import { FetchError, FormattableLogMessage, IConfigFetcher, LogLevel } from "../../src"; 6 | import type { IFetchResponse, OptionsBase } from "../../src"; 7 | 8 | export class HttpConfigFetcher implements IConfigFetcher { 9 | private handleResponse(response: http.IncomingMessage, resolve: (value: IFetchResponse) => void, reject: (reason?: any) => void) { 10 | try { 11 | const { statusCode, statusMessage: reasonPhrase } = response as { statusCode: number; statusMessage: string }; 12 | 13 | if (statusCode === 200) { 14 | const eTag = response.headers["etag"]; 15 | const chunks: any[] = []; 16 | response 17 | .on("data", chunk => chunks.push(chunk)) 18 | .on("end", () => { 19 | try { 20 | resolve({ statusCode, reasonPhrase, eTag, body: Buffer.concat(chunks).toString() }); 21 | } 22 | catch (err) { 23 | reject(err); 24 | } 25 | }) 26 | .on("error", err => reject(new FetchError("failure", err))); 27 | } 28 | else { 29 | // Consume response data to free up memory 30 | response.resume(); 31 | 32 | resolve({ statusCode, reasonPhrase }); 33 | } 34 | } 35 | catch (err) { 36 | reject(err); 37 | } 38 | } 39 | 40 | fetchLogic(options: OptionsBase, lastEtag: string): Promise { 41 | return new Promise((resolve, reject) => { 42 | try { 43 | options.logger.debug("HttpConfigFetcher.fetchLogic() called."); 44 | const baseUrl = options.getUrl(); 45 | const isBaseUrlSecure = baseUrl.startsWith("https"); 46 | let agent: any; 47 | if (options.proxy) { 48 | try { 49 | const proxy: URL = new URL(options.proxy); 50 | let agentFactory: any; 51 | if (proxy.protocol === "https:") { 52 | agentFactory = isBaseUrlSecure ? tunnel.httpsOverHttps : tunnel.httpOverHttps; 53 | } 54 | else { 55 | agentFactory = isBaseUrlSecure ? tunnel.httpsOverHttp : tunnel.httpOverHttp; 56 | } 57 | agent = agentFactory({ 58 | proxy: { 59 | host: proxy.hostname, 60 | port: proxy.port, 61 | proxyAuth: (proxy.username && proxy.password) ? `${proxy.username}:${proxy.password}` : null 62 | } 63 | }); 64 | } 65 | catch (err) { 66 | options.logger.log(LogLevel.Error, 0, FormattableLogMessage.from("PROXY")`Failed to parse \`options.proxy\`: '${options.proxy}'.`, err); 67 | } 68 | } 69 | 70 | const requestOptions = { 71 | agent, 72 | headers: { 73 | "User-Agent": options.clientVersion, 74 | "If-None-Match": lastEtag ?? null 75 | }, 76 | timeout: options.requestTimeoutMs, 77 | }; 78 | options.logger.debug(JSON.stringify(requestOptions)); 79 | 80 | const request = (isBaseUrlSecure ? https : http).get(baseUrl, requestOptions, response => this.handleResponse(response, resolve, reject)) 81 | .on("timeout", () => { 82 | try { 83 | request.destroy(); 84 | } 85 | finally { 86 | reject(new FetchError("timeout", options.requestTimeoutMs)); 87 | } 88 | }) 89 | .on("error", err => reject(new FetchError("failure", err))) 90 | .end(); 91 | } 92 | catch (err) { 93 | reject(err); 94 | } 95 | }); 96 | } 97 | } 98 | 99 | export default IConfigFetcher; 100 | -------------------------------------------------------------------------------- /test/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { IAutoPollOptions, IConfigCatKernel, ILazyLoadingOptions, IManualPollOptions } from "../../src"; 2 | import { ConfigCatClient, IConfigCatClient } from "../../src/ConfigCatClient"; 3 | import { AutoPollOptions, LazyLoadOptions, ManualPollOptions } from "../../src/ConfigCatClientOptions"; 4 | 5 | export const sdkType = "ConfigCat-JS-Common", sdkVersion = "0.0.0-test"; 6 | 7 | export function createClientWithAutoPoll(sdkKey: string, configCatKernel: IConfigCatKernel, options?: IAutoPollOptions): IConfigCatClient { 8 | return new ConfigCatClient(new AutoPollOptions(sdkKey, configCatKernel.sdkType, configCatKernel.sdkVersion, options, configCatKernel.defaultCacheFactory, configCatKernel.eventEmitterFactory), configCatKernel); 9 | } 10 | 11 | export function createClientWithManualPoll(sdkKey: string, configCatKernel: IConfigCatKernel, options?: IManualPollOptions): IConfigCatClient { 12 | return new ConfigCatClient(new ManualPollOptions(sdkKey, configCatKernel.sdkType, configCatKernel.sdkVersion, options, configCatKernel.defaultCacheFactory, configCatKernel.eventEmitterFactory), configCatKernel); 13 | } 14 | 15 | export function createClientWithLazyLoad(sdkKey: string, configCatKernel: IConfigCatKernel, options?: ILazyLoadingOptions): IConfigCatClient { 16 | return new ConfigCatClient(new LazyLoadOptions(sdkKey, configCatKernel.sdkType, configCatKernel.sdkVersion, options, configCatKernel.defaultCacheFactory, configCatKernel.eventEmitterFactory), configCatKernel); 17 | } 18 | 19 | // See https://stackoverflow.com/a/72237137/8656352 20 | export function allowEventLoop(waitMs = 0): Promise { 21 | return new Promise(resolve => setTimeout(resolve, waitMs)); 22 | } 23 | 24 | export function normalizeLineEndings(text: string, eol = "\n"): string { 25 | return text.replace(/\r\n?|\n/g, eol); 26 | } 27 | 28 | export function escapeRegExp(text: string): string { 29 | // See also: https://tc39.es/ecma262/#prod-SyntaxCharacter 30 | return text.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"); 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.build.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "module": "CommonJS", 6 | "outDir": "./lib", 7 | } 8 | } -------------------------------------------------------------------------------- /tsconfig.build.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "module": "ES2015", 6 | "outDir": "./lib/esm", 7 | } 8 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "declarationDir": "./lib" 8 | }, 9 | "include": [ 10 | "src/**/*.ts", 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Common TS compiler settings for compilation outputs, IDEs (like VSCode) and test runner (Mocha) */ 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "ESNext", 6 | "importHelpers": true, 7 | "moduleResolution": "Node", 8 | "lib": [ 9 | "ES2015", // APIs used: Promise (.race) 10 | "DOM", // APIs used: setTimeout, clearTimeout 11 | "ES2017.Object", // APIs used: Object.values (polyfilled), Object.entries (polyfilled) 12 | "ES2019.Object", // APIs used: Object.fromEntries (polyfilled) 13 | // TODO: use the following built-in libs instead of the manually included lib.*.d.ts files when upgrading TypeScript to v4.3 or newer 14 | //"ES2021.WeakRef", // APIs used: WeakRef (only used if available) 15 | //"ES2021.Promise" // APIs used: AggregateError (only used if available) 16 | ] 17 | }, 18 | "exclude": [ 19 | "**/node_modules/", 20 | "lib/", 21 | "samples/" 22 | ], 23 | } -------------------------------------------------------------------------------- /tsconfig.mocha.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2018", 5 | "module": "CommonJS", 6 | "downlevelIteration": true, 7 | }, 8 | "ts-node": { 9 | "files": true 10 | }, 11 | "files": [ 12 | "src/lib.es2021.promise.d.ts", 13 | "src/lib.es2021.weakref.d.ts" 14 | ] 15 | } --------------------------------------------------------------------------------