├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── commitlint.config.js ├── config ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.types.json ├── tsconfig.umd.json └── webpack.config.js ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ ├── style.css │ ├── widgets.png │ └── widgets@2x.png ├── classes │ ├── FPUser.html │ └── FeatureProbe.html ├── functions │ ├── getPlatform.html │ ├── initializePlatform.html │ └── setPlatform.html ├── index.html ├── interfaces │ ├── FPConfig.html │ ├── FPDetail.html │ ├── FPStorageProvider.html │ ├── IHttpRequest.html │ ├── IOption.html │ └── IPlatForm.html └── types │ └── IReturnValue.html ├── example └── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── setupJest.js ├── src ├── EventRecorder.ts ├── FPUser.ts ├── FeatureProbe.ts ├── autoReportEvents.ts ├── flushEvents.ts ├── index.ts ├── localStorage.ts ├── platform.ts └── types.ts ├── test ├── FPUser.test.ts ├── FeatureProbe.test.ts ├── autoReportEvents.test.ts ├── fixtures │ ├── events.json │ └── toggles.json ├── flushEvent.test.ts ├── index.test.ts └── localStorage.test.ts ├── tools ├── cleanup.js └── packagejson.js ├── tsconfig.json └── typedoc.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/* 2 | tools/* 3 | config/* 4 | docs/* 5 | jest.config.js 6 | segupJest.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/explicit-function-return-type": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '29 17 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm run test:cov 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v3 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm run build --if-present 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | build/ 3 | coverage 4 | node_modules/ 5 | yarn.lock 6 | .DS_Store 7 | lib 8 | .idea 9 | .vscode/ 10 | .husky/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FeatureProbe Client Side SDK for JavaScript 2 | 3 | [![Top Language](https://img.shields.io/github/languages/top/FeatureProbe/client-sdk-js)](https://github.com/FeatureProbe/client-sdk-js/search?l=rust) 4 | [![Coverage Status](https://coveralls.io/repos/github/FeatureProbe/client-sdk-js/badge.svg?branch=main)](https://coveralls.io/github/FeatureProbe/client-sdk-js?branch=main) 5 | [![Github Star](https://img.shields.io/github/stars/FeatureProbe/client-sdk-js)](https://github.com/FeatureProbe/client-sdk-js/stargazers) 6 | [![Apache-2.0 license](https://img.shields.io/github/license/FeatureProbe/FeatureProbe)](https://github.com/FeatureProbe/FeatureProbe/blob/main/LICENSE) 7 | 8 | [FeatureProbe](https://featureprobe.com/) is an open source feature management service. This SDK is used to control features in JavaScript programs. This 9 | SDK is designed primarily for use in multi-user systems such as web servers and applications. 10 | 11 | ## Basic Terms 12 | 13 | Reading the short [Basic Terms](https://github.com/FeatureProbe/FeatureProbe/blob/main/BASIC_TERMS.md) will help to understand the code blow more easily. [中文](https://github.com/FeatureProbe/FeatureProbe/blob/main/BASIC_TERMS_CN.md) 14 | 15 | ## Core Data Structures 16 | 17 | Reading the short [Doc](https://github.com/FeatureProbe/feature-probe-docs/blob/b8c55a35c771e4223469f1b121f8b78ab3d9bc22/docs/sdk/sdk-introduction.md?plain=1#L13-L34) about core data sturtures. [中文](https://github.com/FeatureProbe/feature-probe-docs/blob/b8c55a35c771e4223469f1b121f8b78ab3d9bc22/i18n/zh-CN/docusaurus-plugin-content-docs/current/sdk/sdk-introduction.md?plain=1#L14-L35) 18 | 19 | ## How to use this SDK 20 | 21 | See [SDK Doc](https://featureprobe.github.io/FeatureProbe/how-to/Client-Side%20SDKs/javascript-sdk/) for detail([中文](https://featureprobe.github.io/FeatureProbe/zh-CN/how-to/Client-Side%20SDKs/javascript-sdk)). For more information about SDK API, please reference [SDK API](https://featureprobe.github.io/client-sdk-js/). 22 | 23 | 24 | ## Contributing 25 | 26 | We are working on continue evolving FeatureProbe core, making it flexible and easier to use. 27 | Development of FeatureProbe happens in the open on GitHub, and we are grateful to the 28 | community for contributing bugfixes and improvements. 29 | 30 | Please read [CONTRIBUTING](https://github.com/FeatureProbe/featureprobe/blob/master/CONTRIBUTING.md) 31 | for details on our code of conduct, and the process for taking part in improving FeatureProbe. 32 | 33 | ## License 34 | 35 | This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. 36 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /config/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../dist/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "outDir": "../dist/esm" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "../dist/types" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /config/tsconfig.umd.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/index.ts', 6 | output: { 7 | path: path.resolve(__dirname, '../dist/umd'), 8 | filename: 'index.js', 9 | library: 'exampleTypescriptPackage', 10 | libraryTarget: 'umd', 11 | globalObject: 'this', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.ts(x*)?$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'ts-loader', 20 | options: { 21 | configFile: 'config/tsconfig.umd.json', 22 | }, 23 | }, 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: ['.ts', '.js'], 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-code-background: #FFFFFF; 3 | --dark-code-background: #1E1E1E; 4 | } 5 | 6 | @media (prefers-color-scheme: light) { :root { 7 | --code-background: var(--light-code-background); 8 | } } 9 | 10 | @media (prefers-color-scheme: dark) { :root { 11 | --code-background: var(--dark-code-background); 12 | } } 13 | 14 | :root[data-theme='light'] { 15 | --code-background: var(--light-code-background); 16 | } 17 | 18 | :root[data-theme='dark'] { 19 | --code-background: var(--dark-code-background); 20 | } 21 | 22 | pre, code { background: var(--code-background); } 23 | -------------------------------------------------------------------------------- /docs/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FeatureProbe/client-sdk-js/0cbb523585b2b90a2b9a99f1222deb5c7f064468/docs/assets/widgets.png -------------------------------------------------------------------------------- /docs/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FeatureProbe/client-sdk-js/0cbb523585b2b90a2b9a99f1222deb5c7f064468/docs/assets/widgets@2x.png -------------------------------------------------------------------------------- /docs/functions/getPlatform.html: -------------------------------------------------------------------------------- 1 | getPlatform | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Function getPlatform

16 |
17 |
23 |
58 |
59 |

Generated using TypeDoc

60 |
-------------------------------------------------------------------------------- /docs/functions/initializePlatform.html: -------------------------------------------------------------------------------- 1 | initializePlatform | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Function initializePlatform

16 |
17 |
    18 | 19 |
  • 20 |

    Initialize SDK with platform

    21 |
    22 |
    23 |

    Parameters

    24 |
      25 |
    • 26 |
      options: IOption
      27 |

      The platform object

      28 |
    29 |

    Returns void

32 |
67 |
68 |

Generated using TypeDoc

69 |
-------------------------------------------------------------------------------- /docs/functions/setPlatform.html: -------------------------------------------------------------------------------- 1 | setPlatform | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Function setPlatform

16 |
17 |
28 |
63 |
64 |

Generated using TypeDoc

65 |
-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 |

FeatureProbe Client Side SDK for JavaScript (2.2.1)

13 |
14 |
15 |

Index

16 |
17 |

Classes

18 |
FPUser 19 | FeatureProbe 20 |
21 |
22 |

Interfaces

23 |
30 |
31 |

Type Aliases

32 |
34 |
35 |

Functions

36 |
40 |
75 |
76 |

Generated using TypeDoc

77 |
-------------------------------------------------------------------------------- /docs/interfaces/FPDetail.html: -------------------------------------------------------------------------------- 1 | FPDetail | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Interface FPDetail

16 |
17 |

Hierarchy

18 |
    19 |
  • FPDetail
22 |
23 |
24 |
25 | 26 |
27 |
28 |

Properties

29 |
38 |
39 |

Properties

40 |
41 | 42 |
debugUntilTime?: number
43 |

Debug deadline timestamp

44 |
47 |
48 | 49 |
lastModified?: number
50 |

Toggle last modified timestamp

51 |
54 |
55 | 56 |
reason: string
57 |

Why return this value, like disabled, default, not exist and so on.

58 |
61 |
62 | 63 |
ruleIndex: null | number
64 |

The sequence number of the rule in the UI configuration that hit the rule.

65 |
68 |
69 | 70 |
trackAccessEvents?: boolean
71 |

Whether to report access events.

72 |
75 |
76 | 77 |
value: string | number | boolean | Record<string, unknown>
78 |

The value corresponding to the rule in the UI platform.

79 |
82 |
83 | 84 |
variationIndex: null | number
85 |

The sequence number of the variation in the UI platform.

86 |
89 |
90 | 91 |
version: null | number
92 |

The version of the toggle.

93 |
96 |
129 |
130 |

Generated using TypeDoc

131 |
-------------------------------------------------------------------------------- /docs/interfaces/FPStorageProvider.html: -------------------------------------------------------------------------------- 1 | FPStorageProvider | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Interface FPStorageProvider

16 |
17 |

Hierarchy

18 |
    19 |
  • FPStorageProvider
22 |
23 |
24 |
25 | 26 |
27 |
28 |

Properties

29 |
getItem 30 | setItem 31 |
32 |
33 |

Properties

34 |
35 | 36 |
getItem: ((key: string) => Promise<string>)
37 |
38 |

Type declaration

39 |
    40 |
  • 41 |
      42 |
    • (key: string): Promise<string>
    • 43 |
    • 44 |

      Get data from storage.

      45 |
      46 |
      47 |

      Parameters

      48 |
        49 |
      • 50 |
        key: string
        51 |

        The key of the storage item.

        52 |
      53 |

      Returns Promise<string>

56 |
57 | 58 |
setItem: ((key: string, data: string) => Promise<void>)
59 |
60 |

Type declaration

61 |
    62 |
  • 63 |
      64 |
    • (key: string, data: string): Promise<void>
    • 65 |
    • 66 |

      Save data to storage.

      67 |
      68 |
      69 |

      Parameters

      70 |
        71 |
      • 72 |
        key: string
        73 |

        The key of the storage item.

        74 |
      • 75 |
      • 76 |
        data: string
        77 |

        The data of the storage item.

        78 |
      79 |

      Returns Promise<void>

82 |
109 |
110 |

Generated using TypeDoc

111 |
-------------------------------------------------------------------------------- /docs/interfaces/IOption.html: -------------------------------------------------------------------------------- 1 | IOption | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Interface IOption

16 |
17 |

Hierarchy

18 |
    19 |
  • IOption
22 |
23 |
24 |
25 | 26 |
27 |
28 |

Properties

29 |
platform 30 |
31 |
32 |

Properties

33 |
34 | 35 |
platform: IPlatForm
38 |
64 |
65 |

Generated using TypeDoc

66 |
-------------------------------------------------------------------------------- /docs/interfaces/IPlatForm.html: -------------------------------------------------------------------------------- 1 | IPlatForm | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Interface IPlatForm

16 |
17 |

Hierarchy

18 |
    19 |
  • IPlatForm
22 |
23 |
24 |
25 | 26 |
27 |
28 |

Properties

29 |
UA 30 | httpRequest 31 | localStorage 32 |
33 |
34 |

Methods

35 |
socket 36 |
37 |
38 |

Properties

39 |
40 | 41 |
UA: string
44 |
45 | 46 |
httpRequest: IHttpRequest
49 |
50 | 51 |
localStorage: FPStorageProvider
54 |
55 |

Methods

56 |
57 | 58 |
    59 | 60 |
  • 61 |
    62 |

    Parameters

    63 |
      64 |
    • 65 |
      uri: string
    • 66 |
    • 67 |
      Optional opts: Partial<ManagerOptions & SocketOptions>
    68 |

    Returns Socket<DefaultEventsMap, DefaultEventsMap>

71 |
100 |
101 |

Generated using TypeDoc

102 |
-------------------------------------------------------------------------------- /docs/types/IReturnValue.html: -------------------------------------------------------------------------------- 1 | IReturnValue | FeatureProbe Client Side SDK for JavaScript (2.2.1)
2 |
3 | 8 |
9 |
10 |
11 |
12 | 15 |

Type alias IReturnValue

16 |
IReturnValue: string | number | boolean | Record<string, unknown>
19 |
54 |
55 |

Generated using TypeDoc

56 |
-------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | FeatureProbe JS SDK demo 12 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

FeatureProbe JS SDK demo

24 |

25 | This is a simple front-end-only page using FeatureProbe JS SDK. 26 |

27 | 28 |

boolean type

29 | 37 |
38 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/test"], 3 | testMatch: [ 4 | "**/__tests__/**/*.+(ts|tsx|js)", 5 | "**/?(*.)+(spec|test).+(ts|tsx|js)", 6 | ], 7 | transform: { 8 | "^.+\\.(ts|tsx)$": "ts-jest", 9 | }, 10 | globals: { 11 | "ts-jest": { 12 | tsconfig: "tsconfig.json", 13 | }, 14 | }, 15 | setupFiles: ["./setupJest.js"], 16 | testEnvironment: "jsdom", 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "featureprobe-client-sdk-js", 3 | "version": "2.2.1", 4 | "description": "FeatureProbe Client Side SDK for JavaScript", 5 | "main": "./dist/src/index.js", 6 | "types": "./dist/src/index.d.ts", 7 | "unpkg": "./dist/featureprobe-client-sdk-js.min.js", 8 | "jsdelivr": "./dist/featureprobe-client-sdk-js.min.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "postinstall": "husky install", 14 | "prepublishOnly": "pinst --disable", 15 | "postpublish": "pinst --enable", 16 | "build": "npm run build:ts && npm run build:web", 17 | "build:ts": "tsc", 18 | "build:web": "rollup -c rollup.config.js", 19 | "clean": "node tools/cleanup", 20 | "package": "npm run build && npm pack", 21 | "test": "jest --no-cache --runInBand", 22 | "test:cov": "jest --coverage --no-cache --runInBand", 23 | "addscope": "node tools/packagejson name featureprobe-client-sdk-js", 24 | "doc": "typedoc" 25 | }, 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "keywords": [ 30 | "featureprobe", 31 | "client" 32 | ], 33 | "license": "Apache-2.0", 34 | "homepage": "https://github.com/FeatureProbe/client-sdk-js", 35 | "repository": { 36 | "type": "git", 37 | "url": "git@github.com:FeatureProbe/client-sdk-js.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/FeatureProbe/client-sdk-js/issues" 41 | }, 42 | "dependencies": { 43 | "js-base64": "^3.7.2", 44 | "socket.io-client": "^4.5.4", 45 | "tiny-emitter": "^2.1.0", 46 | "whatwg-fetch": "^3.6.2" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "^17.6.1", 50 | "@commitlint/config-conventional": "^13.1.0", 51 | "@rollup/plugin-commonjs": "^24.0.0", 52 | "@rollup/plugin-json": "^4.1.0", 53 | "@rollup/plugin-node-resolve": "^15.0.1", 54 | "@types/eslint": "^8.4.2", 55 | "@types/estree": "^0.0.51", 56 | "@types/jest": "^27.0.1", 57 | "@typescript-eslint/eslint-plugin": "^4.31.1", 58 | "@typescript-eslint/parser": "^4.31.1", 59 | "eslint": "^7.32.0", 60 | "eslint-config-prettier": "^8.3.0", 61 | "eslint-plugin-prettier": "^4.0.0", 62 | "husky": "^7.0.2", 63 | "jest": "^27.2.0", 64 | "jest-fetch-mock": "^3.0.3", 65 | "pinst": "^2.1.6", 66 | "prettier": "^2.4.0", 67 | "rollup": "^2.70.2", 68 | "rollup-plugin-babel-minify": "^10.0.0", 69 | "rollup-plugin-node-polyfills": "^0.2.1", 70 | "rollup-plugin-typescript2": "^0.31.2", 71 | "ts-jest": "^27.0.5", 72 | "ts-loader": "^9.2.5", 73 | "typedoc": "^0.23.10", 74 | "typescript": "^4.6.3", 75 | "webpack": "^5.52.1", 76 | "webpack-cli": "^4.8.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import minify from "rollup-plugin-babel-minify"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | import json from "@rollup/plugin-json"; 6 | import nodePolyfills from "rollup-plugin-node-polyfills"; 7 | 8 | export default { 9 | input: "./src/index.ts", 10 | output: [ 11 | { 12 | file: "./dist/featureprobe-client-sdk-js.min.js", 13 | format: "iife", 14 | name: "featureProbe", 15 | }, 16 | ], 17 | plugins: [ 18 | resolve({ 19 | browser: true, 20 | }), 21 | commonjs({ 22 | include: "node_modules/**", 23 | }), 24 | typescript({ tsconfigOverride: { compilerOptions: { module: "ES2015" } } }), 25 | minify({ comments: false }), 26 | json(), 27 | nodePolyfills(), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /setupJest.js: -------------------------------------------------------------------------------- 1 | global.fetch = require('jest-fetch-mock'); 2 | -------------------------------------------------------------------------------- /src/EventRecorder.ts: -------------------------------------------------------------------------------- 1 | import { getPlatform } from "./platform"; 2 | import { IAccessEvent, IAccess, IToggleCounter, ClickEvent, PageViewEvent, AccessEvent, CustomEvent, DebugEvent } from "./types"; 3 | 4 | export class EventRecorder { 5 | private _clientSdkKey: string; 6 | private _eventsUrl: string; 7 | private _closed: boolean; 8 | private _sendAccessQueue: IAccessEvent[]; 9 | private _sendEventQueue: (AccessEvent | CustomEvent | ClickEvent | PageViewEvent)[]; 10 | private _taskQueue: AsyncBlockingQueue>; 11 | private _timer: NodeJS.Timer; 12 | private readonly _dispatch: Promise; 13 | 14 | constructor( 15 | clientSdkKey: string, 16 | eventsUrl: string, 17 | flushInterval: number, 18 | ) { 19 | this._clientSdkKey = clientSdkKey; 20 | this._eventsUrl = eventsUrl; 21 | this._closed = false; 22 | this._sendAccessQueue = []; 23 | this._sendEventQueue = []; 24 | this._taskQueue = new AsyncBlockingQueue>(); 25 | this._timer = setInterval(() => this.flush(), flushInterval); 26 | this._dispatch = this.startDispatch(); 27 | } 28 | 29 | set flushInterval(value: number) { 30 | clearInterval(this._timer); 31 | this._timer = setInterval(() => this.flush(), value); 32 | } 33 | 34 | get accessQueue(): IAccessEvent[] { 35 | return this._sendAccessQueue; 36 | } 37 | 38 | get eventQueue(): (AccessEvent | CustomEvent | ClickEvent | PageViewEvent)[] { 39 | return this._sendEventQueue; 40 | } 41 | 42 | public recordAccessEvent(accessEvent: IAccessEvent): void { 43 | if (this._closed) { 44 | console.warn("Trying to push access record to a closed EventProcessor, omitted"); 45 | return; 46 | } 47 | this._sendAccessQueue.push(accessEvent); 48 | } 49 | 50 | public recordTrackEvent(trackEvents: ClickEvent | PageViewEvent | AccessEvent | CustomEvent | DebugEvent): void { 51 | if (this._closed) { 52 | console.warn("Trying to push access record to a closed EventProcessor, omitted"); 53 | return; 54 | } 55 | this._sendEventQueue.push(trackEvents); 56 | } 57 | 58 | public flush(): void { 59 | if (this._closed) { 60 | console.warn("Trying to flush a closed EventProcessor, omitted"); 61 | return; 62 | } 63 | this._taskQueue.enqueue(this.doFlush()); 64 | } 65 | 66 | public async stop(): Promise { 67 | if (this._closed) { 68 | console.warn("EventProcessor is already closed"); 69 | return; 70 | } 71 | clearInterval(this._timer); 72 | this._closed = true; 73 | this._taskQueue.enqueue(this.doFlush()); 74 | await this._dispatch; 75 | } 76 | 77 | private async startDispatch(): Promise { 78 | while (!this._closed || !this._taskQueue.isEmpty()) { 79 | await this._taskQueue.dequeue(); 80 | } 81 | } 82 | 83 | private prepareSendData(events: IAccessEvent[]): IAccess { 84 | let start = -1, end = -1; 85 | const counters: { [key: string]: IToggleCounter[] } = {}; 86 | for (const event of events) { 87 | if (start < 0 || start < event.time) { 88 | start = event.time; 89 | } 90 | if (end < 0 || end > event.time) { 91 | end = event.time; 92 | } 93 | 94 | if (counters[event.key] === undefined) { 95 | counters[event.key] = []; 96 | } 97 | let added = false; 98 | for (const counter of counters[event.key]) { 99 | if (counter.index === event.index 100 | && counter.version === event.version 101 | && counter.value === event.value) { 102 | counter.count++; 103 | added = true; 104 | break; 105 | } 106 | } 107 | if (!added) { 108 | counters[event.key].push({ 109 | index: event.index, 110 | version: event.version, 111 | value: event.value, 112 | count: 1, 113 | } as IToggleCounter); 114 | } 115 | } 116 | return { 117 | startTime: start, 118 | endTime: end, 119 | counters: counters, 120 | } as IAccess; 121 | } 122 | 123 | private async doFlush(): Promise { 124 | if (this._sendAccessQueue.length === 0 && this._sendEventQueue.length === 0) { 125 | return; 126 | } 127 | const accessEvents = Object.assign([], this._sendAccessQueue); 128 | const trackEvents = Object.assign([], this._sendEventQueue); 129 | 130 | this._sendAccessQueue = []; 131 | this._sendEventQueue = []; 132 | 133 | const eventRepos = [{ 134 | events: trackEvents, 135 | access: accessEvents.length === 0 ? null : this.prepareSendData(accessEvents), 136 | }]; 137 | 138 | return getPlatform().httpRequest.post(this._eventsUrl, { 139 | "Authorization": this._clientSdkKey, 140 | "Content-Type": "application/json", 141 | "UA": getPlatform()?.UA, 142 | }, JSON.stringify(eventRepos), () => { 143 | // Do nothing 144 | }, (error: string) => { 145 | console.error(`FeatureProbe ${getPlatform()?.UA} SDK: Error reporting events: `, error); 146 | }); 147 | 148 | } 149 | } 150 | 151 | // Reference: https://stackoverflow.com/questions/47157428/how-to-implement-a-pseudo-blocking-async-queue-in-js-ts 152 | class AsyncBlockingQueue { 153 | private promises: Promise[]; 154 | private resolvers: ((t: T) => void)[]; 155 | 156 | constructor() { 157 | this.resolvers = []; 158 | this.promises = []; 159 | } 160 | 161 | public enqueue(t: T) { 162 | if (!this.resolvers.length) { 163 | this.add(); 164 | } 165 | this.resolvers.shift()?.(t); 166 | } 167 | 168 | public dequeue(): Promise | undefined { 169 | if (!this.promises.length) { 170 | this.add(); 171 | } 172 | return this.promises.shift(); 173 | } 174 | 175 | public isEmpty() { 176 | return !this.promises.length; 177 | } 178 | 179 | private add() { 180 | this.promises.push(new Promise(resolve => { 181 | this.resolvers.push(resolve); 182 | })); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/FPUser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * You can obtainan a client of FPUser, 3 | * which provides all of the FPUser's functionality. 4 | */ 5 | export class FPUser { 6 | private key: string; 7 | private attrs: { [key: string]: string }; 8 | 9 | constructor(key?: string) { 10 | this.key = String(key || Date.now()); 11 | this.attrs = {}; 12 | } 13 | 14 | /** 15 | * Upload attributes to the FPUser client. 16 | * 17 | * @param attrName 18 | * The attribute key. 19 | * @param attrValue 20 | * The attribute value. 21 | * 22 | */ 23 | public with(attrName: string, attrValue: string): FPUser { 24 | this.attrs[attrName] = attrValue; 25 | return this; 26 | } 27 | 28 | /** 29 | * Get the unique key of the FPUser client. 30 | */ 31 | public getKey(): string { 32 | return this.key; 33 | } 34 | 35 | /** 36 | * Get all attributes of the FPUser client. 37 | * 38 | * @param key 39 | * The uqique FPUser key. 40 | * 41 | */ 42 | public getAttrs(): { [key: string]: string } { 43 | return Object.assign({}, this.attrs); 44 | } 45 | 46 | /** 47 | * Extend several attributes of the FPUser client. 48 | * 49 | * @param attrs 50 | * key-value pairs. 51 | * 52 | */ 53 | public extendAttrs(attrs: { [key: string]: string }): FPUser { 54 | for (const key in attrs) { 55 | this.attrs[key] = attrs[key]; 56 | } 57 | return this; 58 | } 59 | 60 | /** 61 | * Get attribute value of a specified attribute key. 62 | * 63 | * @param attrName 64 | * The attribute key. 65 | * 66 | */ 67 | public get(attrName: string): string | undefined { 68 | return this.attrs[attrName]; 69 | } 70 | 71 | /** 72 | * Change a stable key for the FPUser client. 73 | * 74 | * @param key 75 | * The uqique FPUser key. 76 | * 77 | */ 78 | public stableRollout(key: string): FPUser { 79 | this.key = key; 80 | return this; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/FeatureProbe.ts: -------------------------------------------------------------------------------- 1 | import { TinyEmitter } from "tiny-emitter"; 2 | import { Base64 } from "js-base64"; 3 | import { FPUser } from "./FPUser"; 4 | import { FPDetail, FPStorageProvider, FPConfig, IReturnValue } from "./types"; 5 | import { getPlatform } from "./platform"; 6 | import { EventRecorder } from "./EventRecorder"; 7 | import reportEvents from "./autoReportEvents"; 8 | import flushEventBeforPageUnload from "./flushEvents"; 9 | 10 | const KEY = "repository"; 11 | 12 | export const EVENTS = { 13 | READY: "ready", 14 | ERROR: "error", 15 | UPDATE: "update", 16 | CACHE_READY: "cache_ready", 17 | FETCH_TOGGLE_ERROR: "fetch_toggle_error", 18 | FETCH_EVENT_ERROR: "fetch_event_error", 19 | }; 20 | 21 | const STATUS = { 22 | START: "start", 23 | PENDING: "pending", 24 | READY: "ready", 25 | ERROR: "error", 26 | }; 27 | 28 | const REFRESH_INTERVAL = 1000; 29 | const TIMEOUT_INTERVAL = 10000; 30 | 31 | /** 32 | * You can obtainan a client of FeatureProbe, 33 | * which provides access to all of the SDK's functionality. 34 | */ 35 | class FeatureProbe extends TinyEmitter { 36 | private _togglesUrl: string; 37 | private _eventsUrl: string; 38 | private _getEventsUrl: string; 39 | private _realtimeUrl: string; 40 | private _realtimePath: string; 41 | private _refreshInterval: number; 42 | private _clientSdkKey: string; 43 | private _user: FPUser; 44 | private _toggles: { [key: string]: FPDetail } | undefined; 45 | private _timer?: NodeJS.Timer; 46 | private _timeoutTimer?: NodeJS.Timer; 47 | private _readyPromise: null | Promise; 48 | private _status: string; 49 | private _timeoutInterval: number; 50 | private _storage: FPStorageProvider; 51 | private _eventRecorder: EventRecorder; 52 | private _enableRealtime: boolean; 53 | 54 | constructor({ 55 | remoteUrl, 56 | togglesUrl, 57 | eventsUrl, 58 | realtimeUrl, 59 | realtimePath, 60 | clientSdkKey, 61 | user, 62 | refreshInterval = REFRESH_INTERVAL, 63 | timeoutInterval = TIMEOUT_INTERVAL, 64 | enableAutoReporting = true, 65 | enableRealtime = false, 66 | }: FPConfig) { 67 | super(); 68 | if (!clientSdkKey) { 69 | throw new Error("clientSdkKey is required"); 70 | } 71 | if (refreshInterval <= 0) { 72 | throw new Error("refreshInterval is invalid"); 73 | } 74 | if (timeoutInterval <= 0) { 75 | throw new Error("timeoutInterval is invalid"); 76 | } 77 | if (!remoteUrl && !togglesUrl && !eventsUrl) { 78 | throw new Error("remoteUrl is required"); 79 | } 80 | if (!remoteUrl && !togglesUrl) { 81 | throw new Error("remoteUrl or togglesUrl is required"); 82 | } 83 | if (!remoteUrl && !eventsUrl) { 84 | throw new Error("remoteUrl or eventsUrl is required"); 85 | } 86 | 87 | this._toggles = undefined; 88 | this._togglesUrl = togglesUrl ?? remoteUrl + "/api/client-sdk/toggles"; 89 | this._eventsUrl = eventsUrl ?? remoteUrl + "/api/events"; 90 | this._getEventsUrl = eventsUrl ?? remoteUrl + "/api/client-sdk/events"; 91 | this._realtimeUrl = realtimeUrl ?? remoteUrl + "/realtime"; 92 | this._realtimePath = realtimePath ?? "/server/realtime"; 93 | this._user = user; 94 | this._clientSdkKey = clientSdkKey; 95 | this._refreshInterval = refreshInterval; 96 | this._timeoutInterval = timeoutInterval; 97 | this._status = STATUS.START; 98 | this._storage = getPlatform().localStorage; 99 | this._readyPromise = null; 100 | this._eventRecorder = new EventRecorder(this._clientSdkKey, this._eventsUrl, this._refreshInterval); 101 | this._enableRealtime = enableRealtime; 102 | 103 | if (enableAutoReporting && window && document) { 104 | reportEvents(this); 105 | } 106 | 107 | if (window && document) { 108 | flushEventBeforPageUnload(this._eventRecorder); 109 | } 110 | } 111 | 112 | get clientSdkKey(): string { 113 | return this._clientSdkKey; 114 | } 115 | 116 | get user(): FPUser { 117 | return this._user; 118 | } 119 | 120 | get eventsUrl(): string { 121 | return this._getEventsUrl; 122 | } 123 | 124 | get eventRecorder(): EventRecorder { 125 | return this._eventRecorder; 126 | } 127 | 128 | public static newForTest(toggles: { [key: string]: boolean }): FeatureProbe { 129 | const fp = new FeatureProbe({ 130 | remoteUrl: "http://127.0.0.1:4000", 131 | clientSdkKey: "_", 132 | user: new FPUser(), 133 | timeoutInterval: 1000, 134 | }); 135 | const _toggles: { [key: string]: FPDetail } = {}; 136 | for (const key in toggles) { 137 | const value = toggles[key]; 138 | _toggles[key] = { 139 | value: value, 140 | ruleIndex: null, 141 | variationIndex: null, 142 | version: 0, 143 | reason: "", 144 | }; 145 | } 146 | fp._toggles = _toggles; 147 | fp.successInitialized(); 148 | return fp; 149 | } 150 | 151 | /** 152 | * Start the FeatureProbe client. 153 | */ 154 | public async start(): Promise { 155 | if (this._enableRealtime) { 156 | this.connectSocket(); 157 | } 158 | 159 | if (this._status !== STATUS.START) { 160 | return; 161 | } 162 | 163 | this._status = STATUS.PENDING; 164 | 165 | this._timeoutTimer = setTimeout(() => { 166 | if (this._status === STATUS.PENDING) { 167 | this.errorInitialized(); 168 | } 169 | }, this._timeoutInterval); 170 | 171 | try { 172 | // Emit `cache_ready` event if toggles exist in localStorage 173 | const toggles = await this._storage.getItem(KEY); 174 | if (toggles) { 175 | this._toggles = JSON.parse(toggles); 176 | this.emit(EVENTS.CACHE_READY); 177 | } 178 | 179 | await this.fetchToggles(); 180 | } finally { 181 | this._timer = setInterval(() => this.fetchToggles(), this._refreshInterval); 182 | } 183 | } 184 | 185 | /** 186 | * Stop the FeatureProbe client, once the client has been stopped, 187 | * SDK will no longer listen for toggle changes or send metrics to Server. 188 | */ 189 | public stop(): void { 190 | clearInterval(this._timer); 191 | clearTimeout(this._timeoutTimer); 192 | this._timeoutTimer = undefined; 193 | this._timer = undefined; 194 | } 195 | 196 | /** 197 | * Returns a Promise which tracks the client's ready state. 198 | * 199 | * The Promise will be resolved if the client successfully get toggles from the server 200 | * or ejected if client error get toggles from the server until `timeoutInterval` countdown reaches. 201 | */ 202 | public waitUntilReady(): Promise { 203 | if (this._readyPromise) { 204 | return this._readyPromise; 205 | } 206 | 207 | if (this._status === STATUS.READY) { 208 | return Promise.resolve(); 209 | } 210 | 211 | if (this._status === STATUS.ERROR) { 212 | return Promise.reject(); 213 | } 214 | 215 | this._readyPromise = new Promise((resolve, reject) => { 216 | const onReadyCallback = () => { 217 | this.off(EVENTS.READY, onReadyCallback); 218 | resolve(); 219 | }; 220 | 221 | const onErrorCallback = () => { 222 | this.off(EVENTS.ERROR, onErrorCallback); 223 | reject(); 224 | }; 225 | 226 | this.on(EVENTS.READY, onReadyCallback); 227 | this.on(EVENTS.ERROR, onErrorCallback); 228 | }); 229 | 230 | return this._readyPromise; 231 | } 232 | 233 | /** 234 | * Determines the return `boolean` value of a toggle for the current user. 235 | * 236 | * 237 | * @param key 238 | * The unique key of the toggle. 239 | * @param defaultValue 240 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 241 | */ 242 | public boolValue(key: string, defaultValue: boolean): boolean { 243 | return this.toggleValue(key, defaultValue, "boolean") as boolean; 244 | } 245 | 246 | /** 247 | * Determines the return `number` value of a toggle for the current user. 248 | * 249 | * 250 | * @param key 251 | * The unique key of the toggle. 252 | * @param defaultValue 253 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 254 | */ 255 | public numberValue(key: string, defaultValue: number): number { 256 | return this.toggleValue(key, defaultValue, "number") as number; 257 | } 258 | 259 | /** 260 | * Determines the return `string` value of a toggle for the current user. 261 | * 262 | * 263 | * @param key 264 | * The unique key of the toggle. 265 | * @param defaultValue 266 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 267 | */ 268 | public stringValue(key: string, defaultValue: string): string { 269 | return this.toggleValue(key, defaultValue, "string") as string; 270 | } 271 | 272 | /** 273 | * Determines the return `json` value of a toggle for the current user. 274 | * 275 | * 276 | * @param key 277 | * The unique key of the toggle. 278 | * @param defaultValue 279 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 280 | */ 281 | public jsonValue(key: string, defaultValue: Record): Record { 282 | return this.toggleValue(key, defaultValue, "object") as Record; 283 | } 284 | 285 | /** 286 | * Determines the return `boolean` value of a toggle for the current user, along with information about how it was calculated. 287 | * 288 | * 289 | * @param key 290 | * The unique key of the toggle. 291 | * @param defaultValue 292 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 293 | */ 294 | public boolDetail(key: string, defaultValue: boolean): FPDetail { 295 | return this.toggleDetail(key, defaultValue, "boolean"); 296 | } 297 | 298 | /** 299 | * Determines the return `number` value of a toggle for the current user, along with information about how it was calculated. 300 | * 301 | * 302 | * @param key 303 | * The unique key of the toggle. 304 | * @param defaultValue 305 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 306 | */ 307 | public numberDetail(key: string, defaultValue: number): FPDetail { 308 | return this.toggleDetail(key, defaultValue, "number"); 309 | } 310 | 311 | /** 312 | * Determines the return `string` value of a toggle for the current user, along with information about how it was calculated. 313 | * 314 | * 315 | * @param key 316 | * The unique key of the toggle. 317 | * @param defaultValue 318 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 319 | */ 320 | public stringDetail(key: string, defaultValue: string): FPDetail { 321 | return this.toggleDetail(key, defaultValue, "string"); 322 | } 323 | 324 | /** 325 | * Determines the return `json` value of a toggle for the current user, along with information about how it was calculated. 326 | * 327 | * 328 | * @param key 329 | * The unique key of the toggle. 330 | * @param defaultValue 331 | * The default value of the toggle, to be used if the value is not available from FeatureProbe. 332 | */ 333 | public jsonDetail(key: string, defaultValue: Record): FPDetail { 334 | return this.toggleDetail(key, defaultValue, "object"); 335 | } 336 | 337 | /** 338 | * Returns an object of all available toggles' details to the current user. 339 | */ 340 | public allToggles(): { [key: string]: FPDetail } | undefined { 341 | return Object.assign({}, this._toggles); 342 | } 343 | 344 | /** 345 | * Returns the current user. 346 | * 347 | * This is the user that was most recently passed to [[identifyUser]], or, if [[identifyUser]] has never 348 | * been called, the initial user specified when the client was created. 349 | */ 350 | public getUser(): FPUser { 351 | return this._user; 352 | } 353 | 354 | /** 355 | * Changing the current user to FeatureProbe. 356 | * 357 | * @param user 358 | * A new FPUser instance. 359 | */ 360 | public identifyUser(user: FPUser): void { 361 | this._user = user; 362 | this._toggles = undefined; 363 | this.fetchToggles(); 364 | } 365 | 366 | /** 367 | * Logout the current user, change the current user to an anonymous user. 368 | */ 369 | public logout(): void { 370 | const user = new FPUser(); 371 | this.identifyUser(user); 372 | } 373 | 374 | /** 375 | * Manually push events. 376 | */ 377 | public flush(): void { 378 | this._eventRecorder?.flush(); 379 | } 380 | 381 | /** 382 | * Record custom events, value is optional. 383 | */ 384 | public track(name: string, value?: unknown): void { 385 | this._eventRecorder?.recordTrackEvent({ 386 | kind: "custom", 387 | name, 388 | time: Date.now(), 389 | user: this.getUser().getKey(), 390 | value, 391 | }); 392 | } 393 | 394 | private connectSocket() { 395 | const socket = getPlatform().socket(this._realtimeUrl, { 396 | path: this._realtimePath, 397 | transports: ["websocket"], 398 | }); 399 | 400 | socket.on("connect", () => { 401 | socket.emit("register", { sdk_key: this._clientSdkKey }); 402 | }); 403 | 404 | socket.on("update", () => { 405 | (async () => { 406 | await this.fetchToggles(); 407 | })(); 408 | }); 409 | } 410 | 411 | private toggleValue(key: string, defaultValue: IReturnValue, valueType: string): IReturnValue { 412 | if (this._toggles == undefined) { 413 | return defaultValue; 414 | } 415 | 416 | const detail = this._toggles[key]; 417 | if (detail === undefined) { 418 | return defaultValue; 419 | } 420 | 421 | const v = detail.value; 422 | if (typeof v == valueType) { 423 | const timestamp = Date.now(); 424 | 425 | const DEFAULT_VARIATION_INDEX = -1; 426 | const DEFAULT_VERSION = 0; 427 | 428 | this._eventRecorder?.recordAccessEvent({ 429 | time: timestamp, 430 | key: key, 431 | value: detail.value, 432 | index: detail.variationIndex ?? DEFAULT_VARIATION_INDEX, 433 | version: detail.version ?? DEFAULT_VERSION, 434 | reason: detail.reason, 435 | }); 436 | 437 | if (detail.trackAccessEvents) { 438 | this._eventRecorder?.recordTrackEvent({ 439 | kind: "access", 440 | time: timestamp, 441 | user: this.getUser().getKey(), 442 | key: key, 443 | value: detail.value, 444 | variationIndex: detail.variationIndex ?? DEFAULT_VARIATION_INDEX, 445 | ruleIndex: detail.ruleIndex ?? null, 446 | version: detail.version ?? DEFAULT_VERSION, 447 | }); 448 | } 449 | 450 | if (detail?.debugUntilTime && (Date.now() <= detail?.debugUntilTime)) { 451 | this._eventRecorder?.recordTrackEvent({ 452 | kind: "debug", 453 | time: timestamp, 454 | user: this.getUser().getKey(), 455 | userDetail: this.getUser(), 456 | key: key, 457 | value: detail.value, 458 | variationIndex: detail.variationIndex ?? DEFAULT_VARIATION_INDEX, 459 | ruleIndex: detail.ruleIndex ?? null, 460 | version: detail.version ?? DEFAULT_VERSION, 461 | }); 462 | } 463 | 464 | return v; 465 | } else { 466 | return defaultValue; 467 | } 468 | } 469 | 470 | private toggleDetail( 471 | key: string, 472 | defaultValue: IReturnValue, 473 | valueType: string 474 | ): FPDetail { 475 | if (this._toggles == undefined) { 476 | return { 477 | value: defaultValue, 478 | ruleIndex: null, 479 | variationIndex: null, 480 | version: 0, 481 | reason: "Not ready", 482 | }; 483 | } 484 | 485 | const detail = this._toggles[key]; 486 | if (detail === undefined) { 487 | return { 488 | value: defaultValue, 489 | ruleIndex: null, 490 | variationIndex: null, 491 | version: null, 492 | reason: "Toggle: [" + key + "] not found", 493 | }; 494 | } else if (typeof detail.value === valueType) { 495 | const timestamp = Date.now(); 496 | 497 | this._eventRecorder?.recordAccessEvent({ 498 | time: timestamp, 499 | key: key, 500 | value: detail.value, 501 | index: detail.variationIndex ?? -1, 502 | version: detail.version ?? 0, 503 | reason: detail.reason 504 | }); 505 | 506 | if (detail.trackAccessEvents) { 507 | this._eventRecorder?.recordTrackEvent({ 508 | kind: "access", 509 | time: timestamp, 510 | user: this.getUser().getKey(), 511 | key: key, 512 | value: detail.value, 513 | variationIndex: detail.variationIndex ?? -1, 514 | ruleIndex: detail.ruleIndex ?? null, 515 | version: detail.version ?? 0, 516 | }); 517 | } 518 | 519 | if (detail?.debugUntilTime && (Date.now() <= detail?.debugUntilTime)) { 520 | this._eventRecorder?.recordTrackEvent({ 521 | kind: "debug", 522 | time: timestamp, 523 | user: this.getUser().getKey(), 524 | userDetail: this.getUser(), 525 | key: key, 526 | value: detail.value, 527 | variationIndex: detail.variationIndex ?? -1, 528 | ruleIndex: detail.ruleIndex ?? null, 529 | version: detail.version ?? 0, 530 | }); 531 | } 532 | 533 | return detail; 534 | } else { 535 | return { 536 | value: defaultValue, 537 | ruleIndex: null, 538 | variationIndex: null, 539 | version: null, 540 | reason: "Value type mismatch.", 541 | }; 542 | } 543 | } 544 | 545 | private async fetchToggles() { 546 | const userStr = JSON.stringify(this._user); 547 | const userParam = Base64.encode(userStr); 548 | const url = this._togglesUrl; 549 | 550 | getPlatform().httpRequest.get(url, { 551 | Authorization: this._clientSdkKey, 552 | "Content-Type": "application/json", 553 | UA: getPlatform()?.UA, 554 | }, { 555 | user: userParam, 556 | }, (json: unknown) => { 557 | if (this._status !== STATUS.ERROR) { 558 | this._toggles = json as { [key: string]: FPDetail; } | undefined; 559 | 560 | if (this._status === STATUS.PENDING) { 561 | this.successInitialized(); 562 | } else if (this._status === STATUS.READY) { 563 | this.emit(EVENTS.UPDATE); 564 | } 565 | 566 | this._storage.setItem(KEY, JSON.stringify(json)); 567 | } 568 | }, (error: string) => { 569 | // Emit `fetch_toggle_error` event if toggles are successfully returned from server 570 | this.emit(EVENTS.FETCH_TOGGLE_ERROR); 571 | console.error(`FeatureProbe ${getPlatform()?.UA} SDK: Error getting toggles: `, error); 572 | }) 573 | } 574 | 575 | // Emit `ready` event if toggles are successfully returned from server in time 576 | private successInitialized() { 577 | this._status = STATUS.READY; 578 | setTimeout(() => { 579 | this.emit(EVENTS.READY); 580 | }); 581 | 582 | if (this._timeoutTimer) { 583 | clearTimeout(this._timeoutTimer); 584 | this._timeoutTimer = undefined; 585 | } 586 | } 587 | 588 | // Emit `error` event if toggles are not available and timeout has been reached 589 | private errorInitialized() { 590 | this._status = STATUS.ERROR; 591 | setTimeout(() => { 592 | this.emit(EVENTS.ERROR); 593 | }); 594 | 595 | if (this._timer) { 596 | clearInterval(this._timer); 597 | this._timer = undefined; 598 | } 599 | } 600 | } 601 | 602 | export { FeatureProbe, FPDetail }; 603 | -------------------------------------------------------------------------------- /src/autoReportEvents.ts: -------------------------------------------------------------------------------- 1 | import { getPlatform } from "./platform"; 2 | import { FeatureProbe } from "."; 3 | import { ClickEvent, IEvent, IEventValue, PageViewEvent } from "./types"; 4 | 5 | const WATCH_URL_CHANGE_INTERVAL = 300; 6 | 7 | // Reference: https://github.com/sindresorhus/escape-string-regexp 8 | function escapeStringRegexp(string: string): string { 9 | if (typeof string !== "string") { 10 | throw new TypeError("Expected a string"); 11 | } 12 | 13 | return string 14 | .replace(/[|\\{}()[\]^$+*?.]/g, "\\$&") 15 | .replace(/-/g, "\\x2d"); 16 | } 17 | 18 | /** 19 | * 20 | * Validate whether current page url matches the rule set by event.matcher 21 | * 22 | * @param event 23 | * Event detail 24 | */ 25 | function matchUrl(event: IEventValue): boolean { 26 | const { href, hash, search } = window.location; 27 | let regex; 28 | let testUrl; 29 | 30 | switch (event.matcher) { 31 | case "EXACT": 32 | testUrl = href; 33 | regex = new RegExp("^" + escapeStringRegexp(event.url) + "/?$"); 34 | break; 35 | case "SIMPLE": 36 | testUrl = href.replace(hash, "").replace(search, ""); 37 | regex = new RegExp("^" + escapeStringRegexp(event.url) + "/?$"); 38 | break; 39 | case "SUBSTRING": 40 | testUrl = href.replace(search, ""); 41 | regex = new RegExp(".*" + escapeStringRegexp(event.url) + ".*$"); 42 | break; 43 | case "REGULAR": 44 | testUrl = href.replace(search, ""); 45 | regex = new RegExp(event.url); 46 | break; 47 | default: 48 | return false; 49 | } 50 | 51 | return regex.test(testUrl); 52 | } 53 | 54 | export default function reportEvents(client: FeatureProbe): void { 55 | const { clientSdkKey, user, eventsUrl, eventRecorder } = client; 56 | let previousUrl: string = window.location.href; 57 | let currentUrl; 58 | let cb: (event: MouseEvent) => void; 59 | let totalEvents: IEvent; 60 | 61 | /** 62 | * 63 | * Report different events to Server API 64 | * 65 | * @param kind 66 | * Event type, like click, pageview, etc. 67 | * @param event 68 | * Event detail 69 | */ 70 | function sendEvents(kind: string, event: IEventValue) { 71 | const sendEvent: PageViewEvent = { 72 | kind: kind, 73 | name: event.name, 74 | time: Date.now(), 75 | user: user.getKey(), 76 | url: window.location.href, 77 | }; 78 | 79 | if (kind === "click" && event.selector) { 80 | (sendEvent as ClickEvent).selector = event.selector; 81 | } 82 | 83 | eventRecorder?.recordTrackEvent(sendEvent); 84 | } 85 | 86 | /** 87 | * 88 | * Find the element that was clicked by event bubbling 89 | * 90 | * @param event 91 | * Mouse event 92 | * @param clickEvents 93 | * All click events 94 | * @returns 95 | * Match click events 96 | */ 97 | function getClickEvents(event: MouseEvent, clickEvents: IEventValue[]) { 98 | const matchedEvents = []; 99 | 100 | for (const clickEvent of clickEvents) { 101 | let target = event.target; 102 | const selector = clickEvent.selector; 103 | 104 | const elements = selector && document.querySelectorAll(selector); 105 | 106 | while (target && elements && elements.length > 0) { 107 | for (let j = 0; j < elements.length; j++) { 108 | if (target === elements[j]) { 109 | matchedEvents.push(clickEvent); 110 | } 111 | } 112 | target = ((target).parentNode); 113 | } 114 | } 115 | 116 | return matchedEvents; 117 | } 118 | 119 | /** 120 | * 121 | * First, find all events URLs, if current page url matches one of them, 122 | * send pageview events automatically. 123 | * 124 | * Second, register document click event, if an element is clicked, 125 | * and it's CSS selector matches the event's selector, 126 | * send click event automatically. 127 | * 128 | * 129 | * @param data 130 | * 131 | */ 132 | function distinguishEvents(data: IEvent) { 133 | const clickEvents: IEventValue[] = []; 134 | 135 | for (const key in data) { 136 | const event: IEventValue = data[key]; 137 | 138 | if (matchUrl(event)) { 139 | if (event.type === "PAGE_VIEW") { 140 | sendEvents("pageview", event); 141 | } else if (event.type === "CLICK") { 142 | sendEvents("pageview", event); 143 | clickEvents.push(event); 144 | } 145 | } 146 | } 147 | 148 | if (clickEvents.length > 0) { 149 | cb = function(event: MouseEvent) { 150 | const result = getClickEvents(event, clickEvents); 151 | for (const event of result) { 152 | sendEvents("click", event); 153 | } 154 | }; 155 | 156 | document.addEventListener("click", cb); 157 | } 158 | } 159 | 160 | /** 161 | * 162 | * Watch the change of the page url. 163 | * If it changes, register the pageview events and click events again. 164 | * 165 | */ 166 | function watchUrlChange() { 167 | currentUrl = window.location.href; 168 | 169 | if (currentUrl !== previousUrl) { 170 | previousUrl = currentUrl; 171 | document.removeEventListener("click", cb); 172 | cb = function() { 173 | // do nothing 174 | } 175 | 176 | distinguishEvents(totalEvents); 177 | } 178 | } 179 | 180 | /** 181 | * Register popstate event when using history router 182 | */ 183 | window.addEventListener("popstate", watchUrlChange); 184 | 185 | /** 186 | * Register hashchange event when using hash router 187 | */ 188 | window.addEventListener("hashchange", watchUrlChange); 189 | 190 | /** 191 | * If popstate or hashchange events are not supported 192 | */ 193 | setInterval(() => { 194 | watchUrlChange(); 195 | }, WATCH_URL_CHANGE_INTERVAL); 196 | 197 | /** 198 | * Get events data from Server API 199 | */ 200 | getPlatform().httpRequest.get(eventsUrl, { 201 | "Authorization": clientSdkKey, 202 | "Content-Type": "application/json", 203 | "UA": getPlatform()?.UA, 204 | }, {}, res => { 205 | if (res) { 206 | distinguishEvents(res as IEvent); 207 | totalEvents = res as IEvent; 208 | } 209 | }, (error: string) => { 210 | client.emit("fetch_event_error"); 211 | console.error(`FeatureProbe ${getPlatform()?.UA} SDK: Error getting events: `, error); 212 | }) 213 | } 214 | -------------------------------------------------------------------------------- /src/flushEvents.ts: -------------------------------------------------------------------------------- 1 | import { EventRecorder } from "./EventRecorder"; 2 | 3 | export default function flushEventBeforPageUnload( 4 | eventRecorder: EventRecorder, 5 | ): void { 6 | 7 | const flushHandler = () => { 8 | eventRecorder.flush(); 9 | }; 10 | 11 | window.addEventListener("beforeunload", flushHandler); 12 | window.addEventListener("unload", flushHandler); 13 | window.addEventListener("pagehide", flushHandler); 14 | 15 | document.addEventListener("visibilitychange", function() { 16 | if (document.visibilityState !== "visible") { 17 | flushHandler(); 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { FPUser } from "./FPUser"; 2 | import { FeatureProbe } from "./FeatureProbe"; 3 | import { FPConfig, FPStorageProvider, IOption, FPDetail, IHttpRequest, IReturnValue, IPlatForm } from "./types"; 4 | import { setPlatform, getPlatform } from "./platform"; 5 | 6 | /** 7 | * Initialize SDK with platform 8 | * 9 | * @param options 10 | * The platform object 11 | */ 12 | function initializePlatform(options: IOption): void { 13 | if (options.platform) { 14 | setPlatform(options.platform); 15 | } 16 | } 17 | 18 | export { 19 | FPUser, 20 | FeatureProbe, 21 | FPDetail, 22 | FPConfig, 23 | FPStorageProvider, 24 | IHttpRequest, 25 | IReturnValue, 26 | IOption, 27 | IPlatForm, 28 | initializePlatform, 29 | getPlatform, 30 | setPlatform, 31 | }; 32 | -------------------------------------------------------------------------------- /src/localStorage.ts: -------------------------------------------------------------------------------- 1 | export default class StorageProvider { 2 | public async setItem(key: string, data: string): Promise { 3 | try { 4 | localStorage.setItem(key, data); 5 | } catch (ex) { 6 | console.error(ex); 7 | } 8 | } 9 | 10 | public async getItem(key: string): Promise { 11 | try { 12 | return localStorage.getItem(key) || ""; 13 | } catch (e) { 14 | console.error(e); 15 | return ""; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/platform.ts: -------------------------------------------------------------------------------- 1 | import "whatwg-fetch"; 2 | import { IHttpRequest, IPlatForm } from "./types"; 3 | import { io } from "socket.io-client"; 4 | import StorageProvider from "./localStorage"; 5 | import pkg from "../package.json"; 6 | 7 | const PKG_VERSION = pkg.version; 8 | const UA = "JS/" + PKG_VERSION; 9 | 10 | const httpRequest:IHttpRequest = { 11 | get: function(url, headers, data, successCb, errorCb) { 12 | fetch(url.toString() + "?" + new URLSearchParams(data), { 13 | method: "GET", 14 | cache: "no-cache", 15 | headers: headers, 16 | }) 17 | .then(response => { 18 | if (response.status >= 200 && response.status < 300) { 19 | return response; 20 | } else { 21 | const error: Error = new Error(response.statusText); 22 | throw error; 23 | } 24 | }) 25 | .then(response => response.json()) 26 | .then(json => { 27 | successCb(json); 28 | }) 29 | .catch(e => { 30 | errorCb(e); 31 | }); 32 | }, 33 | post: function(url, headers, data, successCb, errorCb) { 34 | fetch(url.toString(), { 35 | method: "POST", 36 | cache: "no-cache", 37 | headers: headers, 38 | body: data, 39 | keepalive: true, 40 | }) 41 | .then(response => { 42 | if (response.status >= 200 && response.status < 300) { 43 | return response; 44 | } else { 45 | const error: Error = new Error(response.statusText); 46 | throw error; 47 | } 48 | }) 49 | // .then(response => response.json()) 50 | .then(() => { 51 | successCb(); 52 | }) 53 | .catch(e => { 54 | errorCb(e); 55 | }); 56 | } 57 | }; 58 | 59 | const Platform: { 60 | default: IPlatForm; 61 | } = { 62 | default: { 63 | UA: UA, 64 | localStorage: new StorageProvider(), 65 | httpRequest: httpRequest, 66 | socket: io, 67 | } 68 | }; 69 | 70 | function setPlatform(platform: IPlatForm): void { 71 | Platform.default = platform; 72 | } 73 | 74 | function getPlatform(): IPlatForm { 75 | return Platform.default; 76 | } 77 | 78 | export { getPlatform, setPlatform }; 79 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { FPUser } from "./FPUser"; 2 | import { Socket, ManagerOptions, SocketOptions } from "socket.io-client"; 3 | 4 | export interface IAccessEvent { 5 | time: number; 6 | key: string; 7 | value: boolean | string | number | Record; 8 | index: number; 9 | version: number; 10 | reason: string | null; 11 | } 12 | 13 | export interface IToggleCounter { 14 | value: boolean | string | number | Record; 15 | version: number; 16 | index: number; 17 | count: number; 18 | } 19 | 20 | export interface IAccess { 21 | startTime: number; 22 | endTime: number; 23 | counters: { [key: string]: IToggleCounter[] }; 24 | } 25 | 26 | export type IReturnValue = string | number | boolean | Record; 27 | 28 | export interface FPDetail { 29 | /** 30 | * The value corresponding to the rule in the UI platform. 31 | */ 32 | value: boolean | string | number | Record; 33 | 34 | /** 35 | * The sequence number of the rule in the UI configuration that hit the rule. 36 | */ 37 | ruleIndex: number | null; 38 | 39 | /** 40 | * The sequence number of the variation in the UI platform. 41 | */ 42 | variationIndex: number | null; 43 | 44 | /** 45 | * The version of the toggle. 46 | */ 47 | version: number | null; 48 | 49 | /** 50 | * Why return this value, like disabled, default, not exist and so on. 51 | */ 52 | reason: string; 53 | 54 | /** 55 | * Whether to report access events. 56 | */ 57 | trackAccessEvents?: boolean; 58 | 59 | /** 60 | * Toggle last modified timestamp 61 | */ 62 | lastModified?: number; 63 | 64 | /** 65 | * Debug deadline timestamp 66 | */ 67 | debugUntilTime?: number; 68 | } 69 | 70 | export interface FPConfig { 71 | /** 72 | * The unified URL to connect FeatureProbe Server. 73 | */ 74 | remoteUrl?: string; 75 | 76 | /** 77 | * The specific URL to get toggles, if not set, will generate from remoteUrl. 78 | */ 79 | togglesUrl?: string; 80 | 81 | /** 82 | * The specific URL to post events, if not set, will generate from remoteUrl. 83 | */ 84 | eventsUrl?: string; 85 | 86 | /** 87 | * The specific URL to receive realtime events, if not set, will generate from remoteUrl. 88 | */ 89 | realtimeUrl?: string; 90 | 91 | /** 92 | * The specific path to receive realtime events, if not set, default value will be used. 93 | */ 94 | realtimePath?: string; 95 | 96 | /** 97 | * The Client SDK Key is used to authentification. 98 | */ 99 | clientSdkKey: string; 100 | 101 | /** 102 | * The User with attributes like name, age is used when toggle evaluation. 103 | */ 104 | user: FPUser; 105 | 106 | /** 107 | * Milliseconds for SDK to check for update. 108 | */ 109 | refreshInterval?: number; 110 | 111 | /** 112 | * Milliseconds for SDK to initialize, SDK will emit an `error` event when milliseconds reach. 113 | */ 114 | timeoutInterval?: number; 115 | 116 | /** 117 | * Whether SDK should report pageview and click event automatically. Default value is true. 118 | */ 119 | enableAutoReporting?: boolean; 120 | 121 | /** 122 | * Whether SDK should use WebSocket. Default value is false. 123 | */ 124 | enableRealtime?: boolean; 125 | } 126 | 127 | export interface FPStorageProvider { 128 | /** 129 | * Save data to storage. 130 | * 131 | * @param key 132 | * The key of the storage item. 133 | * 134 | * @param data 135 | * The data of the storage item. 136 | */ 137 | setItem: (key: string, data: string) => Promise; 138 | 139 | /** 140 | * Get data from storage. 141 | * 142 | * @param key 143 | * The key of the storage item. 144 | */ 145 | getItem: (key: string) => Promise; 146 | } 147 | 148 | export interface IHttpRequest { 149 | get: ( 150 | url: string, 151 | headers: Record, 152 | data: Record, 153 | successCb: (json: unknown) => void, 154 | errorCb: (e: string) => void 155 | ) => void 156 | post: ( 157 | url: string, 158 | headers: Record, 159 | data: string, 160 | successCb: () => void, 161 | errorCb: (e: string) => void 162 | ) => void 163 | } 164 | 165 | export interface IPlatForm { 166 | localStorage: FPStorageProvider; 167 | UA: string; 168 | httpRequest: IHttpRequest; 169 | socket(uri: string, opts?: Partial): Socket; 170 | } 171 | 172 | export interface IOption { 173 | platform: IPlatForm 174 | } 175 | 176 | export interface AccessEvent { 177 | kind: string; 178 | time: number; 179 | user: string; 180 | key: string; 181 | value: boolean | string | number | Record; 182 | variationIndex: number; 183 | ruleIndex: number | null; 184 | version: number; 185 | } 186 | 187 | export interface DebugEvent { 188 | kind: string; 189 | time: number; 190 | user: string; 191 | userDetail: FPUser; 192 | key: string; 193 | value: boolean | string | number | Record; 194 | variationIndex: number; 195 | ruleIndex: number | null; 196 | version: number; 197 | } 198 | 199 | export interface CustomEvent { 200 | kind: string; 201 | name: string; 202 | time: number; 203 | user: string; 204 | value: unknown; 205 | } 206 | 207 | export interface ClickEvent { 208 | kind: string; 209 | name: string; 210 | time: number; 211 | user: string; 212 | url: string; 213 | selector: string; 214 | } 215 | 216 | export interface PageViewEvent { 217 | kind: string; 218 | name: string; 219 | time: number; 220 | user: string; 221 | url: string; 222 | } 223 | 224 | export interface IEventValue { 225 | matcher: string; 226 | name: string; 227 | type: string; 228 | url: string; 229 | selector?: string; 230 | } 231 | 232 | export interface IEvent { 233 | [key: string]: IEventValue 234 | } 235 | -------------------------------------------------------------------------------- /test/FPUser.test.ts: -------------------------------------------------------------------------------- 1 | import { FPUser } from "../src/index"; 2 | 3 | test("user init", (done) => { 4 | let user = new FPUser(); 5 | const key = user.getKey(); 6 | expect(user.getKey() != undefined); 7 | setTimeout(() => { 8 | const key2 = user.getKey(); 9 | expect(key).toBe(key2); 10 | done(); 11 | }, 10); 12 | }); 13 | 14 | test("user attr", () => { 15 | const user = new FPUser().with("city", "1").with("type", "browser"); 16 | expect(user.get("city")).toBe("1"); 17 | expect(user.get("type")).toBe("browser"); 18 | }); 19 | 20 | test("user attrs", () => { 21 | const attrs = { city: "1" }; 22 | let user = new FPUser().extendAttrs(attrs); 23 | expect(user.getAttrs()).toStrictEqual(attrs); 24 | }); 25 | 26 | test("user stable key", () => { 27 | let user = new FPUser(); 28 | let stableKey = "12jofjaewf"; 29 | user.stableRollout(stableKey); 30 | expect(user.getKey()).toBe(stableKey); 31 | }); 32 | -------------------------------------------------------------------------------- /test/FeatureProbe.test.ts: -------------------------------------------------------------------------------- 1 | import { FeatureProbe, FPUser } from "../src/index"; 2 | import { FetchMock } from "jest-fetch-mock"; 3 | import * as data from "./fixtures/toggles.json"; 4 | 5 | const _fetch = fetch as FetchMock; 6 | let originalError: () => void; 7 | 8 | beforeEach(() => { 9 | originalError = console.error; 10 | console.error = jest.fn(); 11 | }); 12 | 13 | afterEach(() => { 14 | _fetch.resetMocks(); 15 | console.error = originalError; 16 | }); 17 | 18 | test("FeatureProbe init with invalid param", () => { 19 | expect(() => { 20 | new FeatureProbe({ 21 | clientSdkKey: "client-sdk-key1", 22 | user: new FPUser(), 23 | enableAutoReporting: false, 24 | }); 25 | }).toThrow(); 26 | 27 | expect(() => { 28 | new FeatureProbe({ 29 | remoteUrl: "invalid url", 30 | clientSdkKey: "", 31 | user: new FPUser(), 32 | enableAutoReporting: false, 33 | }); 34 | }).toThrow(); 35 | 36 | expect(() => { 37 | new FeatureProbe({ 38 | remoteUrl: "http://127.0.0.1:4007", 39 | clientSdkKey: "client-sdk-key1", 40 | user: new FPUser(), 41 | refreshInterval: -1, 42 | enableAutoReporting: false, 43 | }); 44 | }).toThrow(); 45 | 46 | expect(() => { 47 | new FeatureProbe({ 48 | clientSdkKey: "client-sdk-key1", 49 | user: new FPUser(), 50 | enableAutoReporting: false, 51 | }); 52 | }).toThrow(); 53 | 54 | expect(() => { 55 | new FeatureProbe({ 56 | togglesUrl: "http://127.0.0.1:4007", 57 | clientSdkKey: "client-sdk-key1", 58 | user: new FPUser(), 59 | enableAutoReporting: false, 60 | }); 61 | }).toThrow(); 62 | 63 | expect(() => { 64 | new FeatureProbe({ 65 | eventsUrl: "http://127.0.0.1:4007", 66 | clientSdkKey: "client-sdk-key1", 67 | user: new FPUser(), 68 | enableAutoReporting: false, 69 | }); 70 | }).toThrow(); 71 | }); 72 | 73 | test("FeatureProbe init", () => { 74 | expect( 75 | new FeatureProbe({ 76 | remoteUrl: "http://127.0.0.1:4007", 77 | clientSdkKey: "client-sdk-key1", 78 | user: new FPUser(), 79 | enableAutoReporting: false, 80 | }) 81 | ).not.toBeNull(); 82 | }); 83 | 84 | test("FeatureProbe request", (done) => { 85 | _fetch.mockResponseOnce(JSON.stringify(data)); 86 | const user = new FPUser().with("city", "2"); 87 | let fp = new FeatureProbe({ 88 | remoteUrl: "http://127.0.0.1:4007", 89 | clientSdkKey: "client-sdk-key1", 90 | user: user, 91 | enableAutoReporting: false, 92 | }); 93 | fp.start(); 94 | fp.on("ready", function () { 95 | expect(fp.boolValue("bool_toggle", false)).toBe(true); 96 | fp.stop(); 97 | done(); 98 | }); 99 | }); 100 | 101 | test("FeatureProbe bool toggle", (done) => { 102 | _fetch.mockResponseOnce(JSON.stringify(data)); 103 | const user = new FPUser().with("city", "2"); 104 | let fp = new FeatureProbe({ 105 | remoteUrl: "http://127.0.0.1:4007", 106 | clientSdkKey: "client-sdk-key1", 107 | user: user, 108 | enableAutoReporting: false, 109 | }); 110 | fp.start(); 111 | fp.on("ready", function () { 112 | expect(fp.boolValue("bool_toggle", false)).toBe(true); 113 | expect(fp.boolValue("string_toggle", false)).toBe(false); 114 | expect(fp.boolValue("__not_exist_toggle", false)).toBe(false); 115 | 116 | let detail = fp.boolDetail("bool_toggle", false); 117 | expect(detail.value).toBe(true); 118 | expect(detail.ruleIndex).toBe(0); 119 | 120 | detail = fp.boolDetail("string_toggle", false); 121 | expect(detail.value).toBe(false); 122 | expect(detail.reason).toBe("Value type mismatch."); 123 | done(); 124 | }); 125 | }); 126 | 127 | test("FeatureProbe number toggle", (done) => { 128 | _fetch.mockResponseOnce(JSON.stringify(data)); 129 | const user = new FPUser().with("city", "2"); 130 | let fp = new FeatureProbe({ 131 | remoteUrl: "http://127.0.0.1:4007", 132 | clientSdkKey: "client-sdk-key1", 133 | user: user, 134 | enableAutoReporting: false, 135 | }); 136 | fp.start(); 137 | 138 | fp.on("ready", function () { 139 | expect(fp.numberValue("number_toggle", 0)).toBe(1); 140 | expect(fp.numberValue("string_toggle", 0)).toBe(0); 141 | expect(fp.numberValue("__not_exist_toggle", 1)).toBe(1); 142 | 143 | let detail = fp.numberDetail("number_toggle", 0); 144 | expect(detail.value).toBe(1); 145 | expect(detail.ruleIndex).toBe(0); 146 | 147 | detail = fp.numberDetail("string_toggle", 404); 148 | expect(detail.value).toBe(404); 149 | expect(detail.reason).toBe("Value type mismatch."); 150 | done(); 151 | }); 152 | }); 153 | 154 | test("FeatureProbe string toggle", (done) => { 155 | _fetch.mockResponseOnce(JSON.stringify(data)); 156 | const user = new FPUser().with("city", "2"); 157 | let fp = new FeatureProbe({ 158 | remoteUrl: "http://127.0.0.1:4007", 159 | clientSdkKey: "client-sdk-key1", 160 | user: user, 161 | enableAutoReporting: false, 162 | }); 163 | fp.start(); 164 | fp.on("ready", function () { 165 | expect(fp.stringValue("string_toggle", "ok")).toBe("1"); 166 | expect(fp.stringValue("bool_toggle", "not_match")).toBe("not_match"); 167 | expect(fp.stringValue("__not_exist_toggle", "not_exist")).toBe("not_exist"); 168 | 169 | let detail = fp.stringDetail("bool_toggle", "not match"); 170 | expect(detail.value).toBe("not match"); 171 | expect(detail.reason).toBe("Value type mismatch."); 172 | 173 | detail = fp.stringDetail("string_toggle", "defaultValue"); 174 | expect(detail.value).toBe("1"); 175 | expect(detail.ruleIndex).toBe(0); 176 | 177 | done(); 178 | }); 179 | }); 180 | 181 | test("FeatureProbe json toggle", (done) => { 182 | _fetch.mockResponseOnce(JSON.stringify(data)); 183 | const user = new FPUser().with("city", "2"); 184 | let fp = new FeatureProbe({ 185 | remoteUrl: "http://127.0.0.1:4007", 186 | clientSdkKey: "client-sdk-key1", 187 | user: user, 188 | enableAutoReporting: false, 189 | }); 190 | fp.start(); 191 | 192 | fp.on("ready", function () { 193 | expect(fp.jsonValue("json_toggle", {})).toMatchObject({ 194 | v: "v1", 195 | variation_0: "c2", 196 | }); 197 | expect(fp.jsonValue("bool_toggle", {})).toMatchObject({}); 198 | expect(fp.jsonValue("__not_exist_toggle", {})).toMatchObject({}); 199 | 200 | let detail = fp.jsonDetail("bool_toggle", {}); 201 | expect(detail.value).toMatchObject({}); 202 | expect(detail.reason).toBe("Value type mismatch."); 203 | 204 | detail = fp.jsonDetail("json_toggle", {}); 205 | expect(detail.value).toMatchObject({}); 206 | expect(detail.ruleIndex).toBe(0); 207 | done(); 208 | }); 209 | }); 210 | 211 | test("FeatureProbe all toggle", (done) => { 212 | _fetch.mockResponseOnce(JSON.stringify(data)); 213 | const user = new FPUser().with("city", "2"); 214 | let fp = new FeatureProbe({ 215 | remoteUrl: "http://127.0.0.1:4007", 216 | clientSdkKey: "client-sdk-key1", 217 | user: user, 218 | enableAutoReporting: false, 219 | }); 220 | fp.start(); 221 | 222 | fp.on("ready", function () { 223 | expect(fp.allToggles()).toMatchObject(data); 224 | done(); 225 | }); 226 | }); 227 | 228 | test("FeatureProbe unit testing", (done) => { 229 | let fp = FeatureProbe.newForTest({ testToggle: true }); 230 | 231 | fp.on("ready", function () { 232 | let t = fp.boolValue("testToggle", false); 233 | expect(t).toBe(true); 234 | done(); 235 | }); 236 | }); 237 | 238 | test("FeatureProbe used toggle value before ready", (done) => { 239 | _fetch.mockResponseOnce(JSON.stringify(data)); 240 | const user = new FPUser().with("city", "2"); 241 | let fp = new FeatureProbe({ 242 | remoteUrl: "http://127.0.0.1:4007", 243 | clientSdkKey: "client-sdk-key1", 244 | user: user, 245 | enableAutoReporting: false, 246 | }); 247 | fp.start(); 248 | 249 | expect(fp.boolValue("bool_toggle", false)).toBe(false); 250 | expect(fp.boolDetail("bool_toggle", false)).toMatchObject({ 251 | value: false, 252 | ruleIndex: null, 253 | variationIndex: null, 254 | version: 0, 255 | reason: "Not ready", 256 | }); 257 | done(); 258 | }); 259 | 260 | test("FeatureProbe used toggle value with error key", (done) => { 261 | _fetch.mockResponseOnce(JSON.stringify(data)); 262 | const user = new FPUser().with("city", "2"); 263 | let fp = new FeatureProbe({ 264 | remoteUrl: "http://127.0.0.1:4007", 265 | clientSdkKey: "client-sdk-key1", 266 | user: user, 267 | enableAutoReporting: false, 268 | }); 269 | fp.start(); 270 | 271 | fp.on('ready', () => { 272 | expect(fp.boolValue("error_toggle", false)).toBe(false); 273 | expect(fp.boolDetail("error_toggle", false)).toMatchObject({ 274 | value: false, 275 | ruleIndex: null, 276 | variationIndex: null, 277 | version: null, 278 | reason: "Toggle: [error_toggle] not found", 279 | }); 280 | done(); 281 | }); 282 | }); 283 | 284 | test("FeatureProbe logout", (done) => { 285 | _fetch.mockResponseOnce(JSON.stringify(data)); 286 | const user = new FPUser().with("city", "2"); 287 | expect(user.get('city')).toBe('2'); 288 | let fp = new FeatureProbe({ 289 | remoteUrl: "http://127.0.0.1:4007", 290 | clientSdkKey: "client-sdk-key1", 291 | user: user, 292 | enableAutoReporting: false, 293 | }); 294 | fp.logout(); 295 | expect(fp.getUser().get('city')).toBe(undefined); 296 | done(); 297 | }); 298 | 299 | test("feature promise api", (done) => { 300 | _fetch.mockResponseOnce(JSON.stringify(data)); 301 | let fp = new FeatureProbe({ 302 | remoteUrl: "http://127.0.0.1:4007", 303 | clientSdkKey: "client-sdk-key1", 304 | user: new FPUser(), 305 | enableAutoReporting: false, 306 | }); 307 | 308 | fp.waitUntilReady().then(() => { 309 | done(); 310 | }).catch(() => { 311 | done(); 312 | }); 313 | 314 | fp.start(); 315 | }); 316 | 317 | test("FeatureProbe fetch error", (done) => { 318 | _fetch.mockRejectOnce(new Error("test error")); 319 | 320 | let fp = new FeatureProbe({ 321 | remoteUrl: "http://error.error", 322 | clientSdkKey: "client-sdk-key1", 323 | user: new FPUser(), 324 | refreshInterval: 10000, 325 | enableAutoReporting: false, 326 | }); 327 | fp.start(); 328 | 329 | done(); 330 | }); 331 | 332 | test("FeatureProbe fetch error trigger error event", (done) => { 333 | _fetch.mockReject(new Error("test error")); 334 | 335 | let fp = new FeatureProbe({ 336 | remoteUrl: "http://error.error", 337 | clientSdkKey: "client-sdk-key1", 338 | user: new FPUser(), 339 | refreshInterval: 10000, 340 | timeoutInterval: 1000, 341 | enableAutoReporting: false, 342 | }); 343 | 344 | fp.on('error', () =>{ 345 | done(); 346 | }); 347 | 348 | fp.start(); 349 | }); 350 | -------------------------------------------------------------------------------- /test/autoReportEvents.test.ts: -------------------------------------------------------------------------------- 1 | import reportEvents from "../src/autoReportEvents"; 2 | import { FeatureProbe } from "../src/FeatureProbe"; 3 | import { FPUser } from "../src/FPUser"; 4 | 5 | import { FetchMock } from "jest-fetch-mock"; 6 | import * as data from "./fixtures/events.json"; 7 | 8 | const _fetch = fetch as FetchMock; 9 | let originalError: () => void; 10 | 11 | beforeEach(() => { 12 | originalError = console.error; 13 | console.error = jest.fn(); 14 | }); 15 | 16 | afterEach(() => { 17 | _fetch.resetMocks(); 18 | console.error = originalError; 19 | }); 20 | 21 | test("report events", (done) => { 22 | _fetch.mockResponseOnce(JSON.stringify(data)); 23 | const clientSdkKey = 'clientSdkKey'; 24 | const user = new FPUser('11111').with("city", "2"); 25 | const DELAY = 100; 26 | const fp = new FeatureProbe({ 27 | clientSdkKey, 28 | user, 29 | remoteUrl: 'http://featureprobe.io/server', 30 | }); 31 | 32 | reportEvents(fp); 33 | 34 | setTimeout(() => { 35 | document.body.click(); 36 | expect(fp.eventRecorder.eventQueue.length).toBe(3); 37 | expect(fp.eventRecorder.eventQueue.shift()?.kind).toBe('pageview'); 38 | expect(fp.eventRecorder.eventQueue.shift()?.kind).toBe('pageview'); 39 | expect(fp.eventRecorder.eventQueue.shift()?.kind).toBe('click'); 40 | done(); 41 | }, DELAY); 42 | }); 43 | -------------------------------------------------------------------------------- /test/fixtures/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "5d3ff41f68f14c6c5fccf06487444218": { 3 | "matcher": "SIMPLE", 4 | "name": "5d3ff41f68f14c6c5fccf06487444218", 5 | "type": "PAGE_VIEW", 6 | "url": "http://localhost" 7 | }, 8 | "723fe44736793b2d64ad028e7103e0e5": { 9 | "matcher": "EXACT", 10 | "name": "723fe44736793b2d64ad028e7103e0e5", 11 | "type": "CLICK", 12 | "url": "http://localhost", 13 | "selector": "body" 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/toggles.json: -------------------------------------------------------------------------------- 1 | { 2 | "json_toggle": { 3 | "value": { 4 | "v": "v1", 5 | "variation_0": "c2" 6 | }, 7 | "ruleIndex": 0, 8 | "version": 1, 9 | "reason": "rule 0 " 10 | }, 11 | "multi_condition_toggle": { 12 | "value": { 13 | "variation_0": "" 14 | }, 15 | "ruleIndex": 0, 16 | "version": 1, 17 | "reason": "rule 0 " 18 | }, 19 | "number_toggle": { 20 | "value": 1, 21 | "ruleIndex": 0, 22 | "version": 1, 23 | "reason": "rule 0 " 24 | }, 25 | "disabled_toggle": { 26 | "value": { 27 | "disabled_key": "disabled_value" 28 | }, 29 | "ruleIndex": null, 30 | "version": 1, 31 | "reason": "disabled" 32 | }, 33 | "string_toggle": { 34 | "value": "1", 35 | "ruleIndex": 0, 36 | "version": 1, 37 | "reason": "rule 0 " 38 | }, 39 | "bool_toggle": { 40 | "value": true, 41 | "ruleIndex": 0, 42 | "version": 1, 43 | "reason": "rule 0 " 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/flushEvent.test.ts: -------------------------------------------------------------------------------- 1 | import flushEventBeforPageUnload from "../src/flushEvents"; 2 | import { EventRecorder } from "../src/EventRecorder"; 3 | 4 | 5 | 6 | test("flushEventBeforPageUnload should add event listeners and flush events on page unload", () => { 7 | const INTERVAL = 1000; 8 | 9 | // Make a eventRecorder instance 10 | const eventRecorder = new EventRecorder("sdkKey", "https://www.com", INTERVAL); 11 | 12 | // Spy on the flush method of the mock EventRecorder object 13 | const flushSpy = jest.spyOn(eventRecorder, "flush"); 14 | 15 | // Call the flushEventBeforPageUnload function with the mock EventRecorder object 16 | flushEventBeforPageUnload(eventRecorder); 17 | 18 | // Simulate a page unload event 19 | window.dispatchEvent(new Event("unload")); 20 | 21 | // Expect the EventRecorder's flush method to have been called 22 | expect(flushSpy).toHaveBeenCalled(); 23 | }); 24 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { initializePlatform } from "../src/index"; 2 | import { getPlatform } from "../src/platform" 3 | import StorageProvider from "../src/localStorage"; 4 | import { IHttpRequest } from '../src/types'; 5 | import { io } from "socket.io-client"; 6 | 7 | const httpRequest:IHttpRequest = { 8 | get: function() {}, 9 | post: function() {} 10 | }; 11 | 12 | test("initializePlatform", (done) => { 13 | const platform = { 14 | UA: 'SSS', 15 | httpRequest: httpRequest, 16 | localStorage: new StorageProvider(), 17 | socket: io, 18 | } 19 | 20 | initializePlatform({ 21 | platform: platform 22 | }); 23 | 24 | expect(getPlatform()).toMatchObject(platform); 25 | done(); 26 | }); 27 | -------------------------------------------------------------------------------- /test/localStorage.test.ts: -------------------------------------------------------------------------------- 1 | import StorageProvider from '../src/localStorage'; 2 | 3 | describe('StorageProvider', () => { 4 | const storageProvider = new StorageProvider(); 5 | 6 | beforeEach(() => { 7 | localStorage.clear(); 8 | }); 9 | 10 | test('setItem should set item in localStorage', async () => { 11 | await storageProvider.setItem('testKey', 'testValue'); 12 | expect(localStorage.getItem('testKey')).toBe('testValue'); 13 | }); 14 | 15 | test('getItem should return item from localStorage', async () => { 16 | localStorage.setItem('testKey', 'testValue'); 17 | const result = await storageProvider.getItem('testKey'); 18 | expect(result).toBe('testValue'); 19 | }); 20 | 21 | test('getItem should return empty string if key does not exist', async () => { 22 | const result = await storageProvider.getItem('nonExistentKey'); 23 | expect(result).toBe(''); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tools/cleanup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fs = require('fs') 3 | const Path = require('path') 4 | /* eslint-enable */ 5 | 6 | const deleteFolderRecursive = (path) => { 7 | if (fs.existsSync(path)) { 8 | fs.readdirSync(path).forEach((file) => { 9 | const curPath = Path.join(path, file) 10 | if (fs.lstatSync(curPath).isDirectory()) { 11 | deleteFolderRecursive(curPath) 12 | } else { 13 | fs.unlinkSync(curPath) 14 | } 15 | }) 16 | fs.rmdirSync(path) 17 | } 18 | } 19 | 20 | const folder = process.argv.slice(2)[0] 21 | 22 | if (folder) { 23 | deleteFolderRecursive(Path.join(__dirname, '../dist', folder)) 24 | } else { 25 | deleteFolderRecursive(Path.join(__dirname, '../dist/cjs')) 26 | deleteFolderRecursive(Path.join(__dirname, '../dist/esm')) 27 | deleteFolderRecursive(Path.join(__dirname, '../dist/umd')) 28 | deleteFolderRecursive(Path.join(__dirname, '../dist/types')) 29 | } 30 | -------------------------------------------------------------------------------- /tools/packagejson.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fs = require('fs') 3 | const Path = require('path') 4 | const fileName = '../package.json' 5 | const file = require(fileName) 6 | /* eslint-enable */ 7 | 8 | const args = process.argv.slice(2) 9 | 10 | for (let i = 0, l = args.length; i < l; i++) { 11 | if (i % 2 === 0) { 12 | file[args[i]] = args[i + 1] 13 | } 14 | } 15 | 16 | fs.writeFile( 17 | Path.join(__dirname, fileName), 18 | JSON.stringify(file, null, 2), 19 | (err) => { 20 | if (err) { 21 | return console.log(err) 22 | } 23 | console.log('Writing to ' + fileName) 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "es6"], 5 | "declaration": true, 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["src/**/*"], 15 | } 16 | -------------------------------------------------------------------------------- /typedoc.js: -------------------------------------------------------------------------------- 1 | 2 | const pkg = require('./package.json'); 3 | const PKG_VERSION = pkg.version; 4 | 5 | module.exports = { 6 | out: './docs', 7 | name: 'FeatureProbe Client Side SDK for JavaScript (' + PKG_VERSION + ')', 8 | readme: 'none', 9 | entryPoints: ["./src/index.ts"] 10 | }; --------------------------------------------------------------------------------