├── .all-contributorsrc ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .np-config.json ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASENOTES.md ├── __mocks__ └── axios.js ├── babel.config.js ├── badges ├── badge-functions.svg └── badge-lines.svg ├── cleanDts.sh ├── codecov.yml ├── coverage ├── badge-branches.svg ├── badge-functions.svg ├── badge-lines.svg ├── badge-statements.svg ├── clover.xml └── coverage-final.json ├── doc ├── nodeJSandExpress.png ├── react-ecommerce-demo.gif └── react-with-sdk-js.gif ├── examples ├── api-server │ ├── .DS_Store │ ├── README.md │ ├── app.js │ ├── bin │ │ └── www.js │ ├── mock │ │ └── items.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── stylesheets │ │ │ └── style.css │ ├── routes │ │ ├── checkout.js │ │ ├── fsVisitor.js │ │ ├── index.js │ │ └── items.js │ ├── services │ │ └── flagship.js │ └── webpack.config.js └── react-app │ ├── .env │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── assets │ │ └── Flagship-horizontal-product-white.png │ ├── components │ │ ├── header │ │ │ └── index.tsx │ │ └── modal │ │ │ └── howToCreateToggle │ │ │ └── index.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── serviceWorker.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── assets │ └── img │ │ └── flagshipLogo.jpg ├── class │ ├── bucketing │ │ ├── bucketing.test.ts │ │ ├── bucketing.ts │ │ └── types.ts │ ├── bucketingVisitor │ │ ├── bucketingVisitor.test.ts │ │ ├── bucketingVisitor.ts │ │ └── types.ts │ ├── cacheManager │ │ └── clientCacheManager.ts │ ├── flagship │ │ ├── flagship.test.ts │ │ └── flagship.ts │ ├── flagshipVisitor │ │ ├── flagshipVisitor.test.ts │ │ ├── flagshipVisitor.ts │ │ └── types.ts │ └── panicMode │ │ ├── panicMode.test.ts │ │ ├── panicMode.ts │ │ └── types.ts ├── config │ ├── default.ts │ ├── otherSdk.ts │ ├── test.ts │ └── test_constants.ts ├── index.test.ts ├── index.ts ├── lib │ ├── axiosHelper.ts │ ├── flagshipSdkHelper.ts │ ├── index.test.ts │ ├── loggerHelper.ts │ ├── utils.test.ts │ └── utils.ts └── types.ts ├── test ├── helper │ ├── assertion.ts │ ├── mockGenerator.ts │ └── testUtils.ts └── mock │ ├── bucketing │ ├── index.js │ ├── murmur │ │ ├── arguments.js │ │ ├── badTraffic.js │ │ ├── badTraffic2.js │ │ ├── extremLowTraffic.js │ │ ├── fourVariations.js │ │ └── threeVariations.js │ └── samples │ │ ├── badOperator.js │ │ ├── badTraffic.js │ │ ├── badTypeBetweenTargetingAndVisitorContextKey.js │ │ ├── classical.js │ │ ├── fs_all_users.js │ │ ├── fs_users.js │ │ ├── isoSdk │ │ ├── 25_25_25_25.js │ │ └── 50_50.js │ │ ├── multipleCampaigns │ │ └── index.js │ │ ├── oneCampaignOneVgMultipleTgg │ │ └── index.js │ │ ├── oneCampaignOneVgOneTggWithAllPossibleTargetings │ │ ├── containsOperator.js │ │ ├── endsWithOperator.js │ │ ├── equalsOperator.js │ │ ├── greaterThanOperator.js │ │ ├── greaterThanOrEqualsOperator.js │ │ ├── lowerThanOperator.js │ │ ├── lowerThanOrEqualsOperator.js │ │ ├── notContainsOperator.js │ │ ├── notEqualsOperator.js │ │ └── startsWithOperator.js │ │ ├── oneCampaignWith100PercentAllocation │ │ └── index.js │ │ └── panic.js │ ├── decisionApi │ ├── data │ │ ├── complexJson.js │ │ ├── manyModifInManyCampaigns.js │ │ ├── noModif.js │ │ ├── oneCampaignOneModif.js │ │ ├── oneCampaignOneModifWithAnUpdate.js │ │ ├── oneCampaignWithFurtherModifs.js │ │ ├── oneModifInMoreThanOneCampaign.js │ │ ├── panicMode.js │ │ ├── twoCampaignsWithSameId.js │ │ └── weirdAnswer.js │ └── index.js │ ├── demoData.js │ ├── flagshipVisitor │ └── index.js │ └── hit │ ├── event.js │ ├── item.js │ ├── pageview.js │ ├── screenview.js │ └── transaction.js ├── tsconfig.json ├── webpack.config.js └── webpack ├── config.base.js ├── config.browser.js ├── config.node.js ├── config.reactNative.js ├── config.standalone.js └── config.stats.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "flagship-js-sdk", 3 | "projectOwner": "abtasty", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "gitmoji", 12 | "contributors": [ 13 | { 14 | "login": "Emidomenge", 15 | "name": "Emilien Domenge", 16 | "avatar_url": "https://avatars0.githubusercontent.com/u/15636263?v=4", 17 | "profile": "https://www.domenge.fr/", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "example", 22 | "maintenance" 23 | ] 24 | }, 25 | { 26 | "login": "guillaumejacquart", 27 | "name": "Guillaume Jacquart", 28 | "avatar_url": "https://avatars2.githubusercontent.com/u/5268752?v=4", 29 | "profile": "https://github.com/guillaumejacquart", 30 | "contributions": [ 31 | "bug", 32 | "review" 33 | ] 34 | }, 35 | { 36 | "login": "Madorakkusu", 37 | "name": "Yanis Tam", 38 | "avatar_url": "https://avatars1.githubusercontent.com/u/11974574?v=4", 39 | "profile": "https://yanistam.me/", 40 | "contributions": [ 41 | "code" 42 | ] 43 | }, 44 | { 45 | "login": "Madi-Ji", 46 | "name": "Julien Madiot", 47 | "avatar_url": "https://avatars1.githubusercontent.com/u/6860236?v=4", 48 | "profile": "https://julien-madiot.fr/", 49 | "contributions": [ 50 | "code", 51 | "ideas" 52 | ] 53 | } 54 | ], 55 | "contributorsPerLine": 7 56 | } 57 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | dist/* 3 | node_modules/* 4 | examples/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["airbnb-base", "plugin:@typescript-eslint/recommended", "prettier"], 4 | "env": { 5 | "node": true, 6 | "jest": true, 7 | "es6": true 8 | }, 9 | "globals": { 10 | "localStorage": true 11 | }, 12 | "rules": { 13 | "@typescript-eslint/interface-name-prefix": [ 14 | { 15 | "prefixWithI": "always" 16 | } 17 | ], 18 | "max-len": "off", 19 | "array-callback-return": "off", 20 | "@typescript-eslint/camelcase": 0, 21 | "import/extensions": 0, 22 | "import/prefer-default-export": 1, 23 | "import/no-extraneous-dependencies": 0 24 | }, 25 | "parserOptions": { 26 | "ecmaVersion": "2018", 27 | "sourceType": "module" 28 | }, 29 | "settings": { 30 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 31 | "import/parsers": { 32 | "@typescript-eslint/parser": [".ts", ".tsx"] 33 | }, 34 | "import/resolver": { 35 | "node": { 36 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Code coverage 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | - uses: actions/checkout@master 23 | - name: Install modules 24 | run: npm i 25 | - name: Run tests coverage 26 | id: coverageTest 27 | run: npm run test:coverage 28 | continue-on-error: true 29 | timeout-minutes: 2 30 | - name: Backup coverage test step 31 | if: ${{ steps.coverageTest.conclusion == 'failure' }} 32 | run: npm run test:coverage 33 | - name: Upload coverage to codecov 34 | uses: codecov/codecov-action@v2 35 | with: 36 | #token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 37 | files: ./coverage/clover.xml # optional 38 | flags: unittests # optional 39 | name: codecov-flagship-sdk-coverage # optional 40 | fail_ci_if_error: true # optional (default = false) 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | examples/api-server/dist 4 | 5 | dist 6 | 7 | coverage 8 | 9 | test/flagshipNodeSdkLogs/ 10 | 11 | src/assets/.DS_Store 12 | 13 | stats.json 14 | 15 | *.tgz 16 | 17 | IMPROVEMENTS.md 18 | 19 | .idea 20 | 21 | public/ 22 | -------------------------------------------------------------------------------- /.np-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanup": false 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "tabWidth": 4, 5 | "printWidth": 140, 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest All", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": ["--runInBand"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "windows": { 14 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 15 | } 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Jest Current File", 21 | "program": "${workspaceFolder}/node_modules/.bin/jest", 22 | "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen", 25 | "disableOptimisticBPs": true, 26 | "windows": { 27 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#ef7853", 4 | "activityBar.activeBackground": "#ef7853", 5 | "activityBar.activeBorder": "#8af4a3", 6 | "activityBar.foreground": "#15202b", 7 | "activityBar.inactiveForeground": "#15202b99", 8 | "activityBarBadge.background": "#8af4a3", 9 | "activityBarBadge.foreground": "#15202b", 10 | "titleBar.activeBackground": "#eb5424", 11 | "titleBar.inactiveBackground": "#eb542499", 12 | "titleBar.activeForeground": "#e7e7e7", 13 | "titleBar.inactiveForeground": "#e7e7e799", 14 | "statusBar.background": "#eb5424", 15 | "statusBarItem.hoverBackground": "#ef7853", 16 | "statusBar.foreground": "#e7e7e7" 17 | }, 18 | "peacock.color": "#eb5424", 19 | "javascript.preferences.quoteStyle": "single", 20 | "prettier.singleQuote": true, 21 | "files.autoSave": "onFocusChange", 22 | "eslint.autoFixOnSave": true, 23 | "editor.formatOnPaste": false, 24 | "editor.formatOnSave": true, 25 | "editor.defaultFormatter": "esbenp.prettier-vscode", 26 | "[javascript]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode", 28 | "editor.formatOnSave": true 29 | }, 30 | "[typescript]": { 31 | "editor.defaultFormatter": "esbenp.prettier-vscode", 32 | "editor.formatOnSave": true 33 | }, 34 | "eslint.alwaysShowStatus": true, 35 | "eslint.workingDirectories": [ 36 | { "directory": "examples/react-app", "changeProcessCWD": true }, 37 | { "directory": "", "changeProcessCWD": true } 38 | ], 39 | "editor.codeActionsOnSave": { 40 | "source.fixAll.eslint": true 41 | }, 42 | "prettier.trailingComma": "es5" 43 | } 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Flagship - JS SDK 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 4 | 5 | ## Your dev environment must be 6 | 7 | node v14.0.0 ++ 8 | npm v6.14.4 ++ 9 | 10 | ## Pull Request Process 11 | 12 | 1. Ensure you're able to do a build. 13 | 14 | ``` 15 | flagship-js-sdk$ npm run build 16 | ``` 17 | 18 | 2. Ensure you're able to pass unit test. 19 | 20 | ``` 21 | flagship-js-sdk$ npm run test 22 | ``` 23 | 24 | 3. Consider updating the [README.md](./README.md) with details of changes if needed. 25 | 26 | 4. Add yourself as a contributor. To add yourself to the table of contributors, follow this command: 27 | 28 | ``` 29 | # Add new contributor , who made a contribution of type 30 | npm run contributors:add -- 31 | 32 | # Example: 33 | npm run contributors:add -- jfmengels code,doc 34 | ``` 35 | 36 | See the [Emoji Key (Contribution Types Reference)](https://allcontributors.org/docs/en/emoji-key) for a list of valid contribution types. 37 | 38 | 5. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 39 | do not have permission to do that, you may request the second reviewer to merge it for you. 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | flagship-js-sdk 4 | 5 |

6 | 7 |

Bring your features to life

8 | 9 |
10 | 11 | ![flagship-js-sdk-coverage_lines](badges/badge-lines.svg) 12 | 13 | ![flagship-js-sdk-functions](badges/badge-functions.svg) 14 | 15 | [![](https://data.jsdelivr.com/v1/package/npm/@flagship.io/js-sdk/badge)](https://www.jsdelivr.com/package/npm/@flagship.io/js-sdk) 16 | 17 | 18 | 19 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 20 | 21 | 22 | 23 | [![Apache2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 24 | 25 | [![GitHub release](https://img.shields.io/github/v/release/abtasty/flagship-js-sdk.svg)](https://github.com/abtasty/flagship-js-sdk/releases) 26 | 27 |
28 | 29 | ## Docs 30 | 31 | **Visit [https://developers.flagship.io/](https://developers.flagship.io/) to get started with Flagship.** 32 | 33 | - [Installation & Getting Started](https://developers.flagship.io/docs/sdk/javascript/v2.1#getting-started) 34 | - [API Reference](https://developers.flagship.io/docs/sdk/javascript/v2.1#api-reference) 35 | - [SDK Settings](https://developers.flagship.io/docs/sdk/javascript/v2.1#sdk-settings) 36 | - [Flagship class](https://developers.flagship.io/docs/sdk/javascript/v2.1#flagship-class) 37 | - [Flagship visitor](https://developers.flagship.io/docs/sdk/javascript/v2.1#flagshipvisitor-class) 38 | - [Getting modifications](https://developers.flagship.io/docs/sdk/javascript/v2.1#getting-modifications) 39 | - [Campaign synchronization](https://developers.flagship.io/docs/sdk/javascript/v2.1#campaign-synchronization) 40 | - [Hit tracking](https://developers.flagship.io/docs/sdk/javascript/v2.1#hit-tracking) 41 | - [Release Notes](https://github.com/abtasty/flagship-js-sdk/blob/master/RELEASENOTES.md) 42 | - [Contributing](https://github.com/abtasty/flagship-js-sdk/blob/master/CONTRIBUTING.md) 43 | 44 | ## Examples 45 | 46 | - With Express JS: 47 | 48 |

49 | 50 | node js and express demo 51 | 52 |

53 | 54 |
55 | 56 | [Readme 📖](examples/api-server/README.md) 57 | 58 | **NOTE**: The demo is runnable locally only. 59 | 60 |
61 | 62 | - CodeSandbox with Express JS: 63 | 64 |

65 | 66 | node js and express demo 67 | 68 |

69 | 70 |
71 | 72 | [Live Demo 🕹](https://codesandbox.io/s/flagship-js-sdk-server-example-zuusm) 73 | 74 |
75 | 76 | - With React, inside react component: 77 | 78 |

79 | 80 | react ecommerce demo with cookie 81 | 82 |

83 | 84 |
85 | 86 | [Readme 📖](examples/react-app/README.md) 87 | 88 | [Live Demo 🕹](https://abtasty.github.io/flagship-js-sdk/) 89 | 90 |
91 | 92 | - With React, using cookies: 93 | 94 |

95 | 96 | react ecommerce demo with cookie 97 | 98 |

99 | 100 |
101 | 102 | [Readme 📖](https://github.com/abtasty/flagship-react-sdk/tree/master/examples/react-ecommerce-demo) 103 | 104 | [Live Demo 🕹](https://react-ecommerce-demo.internal.flagship.io/) 105 | 106 |
107 | 108 | - With stand alone version: 109 | 110 |

111 | 112 | js sdk stand alone version 113 | 114 |

115 | 116 |
117 | 118 | [Live Demo 🕹](https://codesandbox.io/s/fs-js-sdk-standalone-version-hoeb9) 119 | 120 |
121 | 122 | ## Contributors 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |

Emilien Domenge

💻 📖 💡 🚧

Guillaume Jacquart

🐛 👀

Yanis Tam

💻

Julien Madiot

💻 🤔
135 | 136 | 137 | 138 | 139 | 140 | 141 | ## Licence 142 | 143 | [Apache License.](https://github.com/abtasty/flagship-js-sdk/blob/master/LICENSE) 144 | -------------------------------------------------------------------------------- /__mocks__/axios.js: -------------------------------------------------------------------------------- 1 | import mockAxios from 'jest-mock-axios'; 2 | 3 | export default mockAxios; 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | 12 | '@babel/preset-typescript', 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /badges/badge-functions.svg: -------------------------------------------------------------------------------- 1 | Coverage:functions: 100%Coverage:functions100% -------------------------------------------------------------------------------- /badges/badge-lines.svg: -------------------------------------------------------------------------------- 1 | Coverage:lines: 100%Coverage:lines100% -------------------------------------------------------------------------------- /cleanDts.sh: -------------------------------------------------------------------------------- 1 | # make single .d.ts file 2 | ./node_modules/.bin/dts-bundle-generator -o ./dist/src/index.d.ts ./dist/src/index.d.ts 3 | 4 | buildPath='./dist/src/' 5 | # delete other *.d.ts files 6 | rm -r "${buildPath}class" 7 | rm -r "${buildPath}config" 8 | rm -r "${buildPath}lib" 9 | rm "${buildPath}types.d.ts" -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: '70...100' 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: 18 | layout: 'reach,diff,flags,tree' 19 | behavior: default 20 | require_changes: no 21 | -------------------------------------------------------------------------------- /coverage/badge-branches.svg: -------------------------------------------------------------------------------- 1 | Coverage:branches: 92.67%Coverage:branches92.67% -------------------------------------------------------------------------------- /coverage/badge-functions.svg: -------------------------------------------------------------------------------- 1 | Coverage:functions: 100%Coverage:functions100% -------------------------------------------------------------------------------- /coverage/badge-lines.svg: -------------------------------------------------------------------------------- 1 | Coverage:lines: 100%Coverage:lines100% -------------------------------------------------------------------------------- /coverage/badge-statements.svg: -------------------------------------------------------------------------------- 1 | Coverage:statements: 100%Coverage:statements100% -------------------------------------------------------------------------------- /doc/nodeJSandExpress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/doc/nodeJSandExpress.png -------------------------------------------------------------------------------- /doc/react-ecommerce-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/doc/react-ecommerce-demo.gif -------------------------------------------------------------------------------- /doc/react-with-sdk-js.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/doc/react-with-sdk-js.gif -------------------------------------------------------------------------------- /examples/api-server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/examples/api-server/.DS_Store -------------------------------------------------------------------------------- /examples/api-server/README.md: -------------------------------------------------------------------------------- 1 | ![Flagship logo](../../src/assets/img/flagshipLogo.jpg) 2 | 3 | # Sample of Express server using [Flagship - JS SDK](../../README.md) 4 | 5 | ### Prerequisites 6 | 7 | - **Node.js**: version 6.0.0 or later... 8 | 9 | - **Npm**: version 3.0.0 or later... 10 | 11 | ``` 12 | 13 | ``` 14 | 15 | ## Getting Started 16 | 17 | - **Install** the node module: 18 | 19 | ``` 20 | examples/api-server$ npm install 21 | ``` 22 | 23 | - **Start** the project: 24 | 25 | on Mac: 26 | 27 | ``` 28 | examples/api-server$ npm run start:mac 29 | ``` 30 | 31 | on Linux: 32 | 33 | ``` 34 | examples/api-server$ npm run start:linux 35 | ``` 36 | 37 | on Windows: 38 | 39 | ``` 40 | examples/api-server$ npm run start:windows 41 | ``` 42 | 43 | ## Run with local Flagship JS SDK 44 | 45 | - You need to link `@flagship.io/js-sdk` : 46 | 47 | - 1 - At the root level (=`PATH/TO/flagship-js-sdk`), run: 48 | 49 | ``` 50 | flagship-js-sdk$ npm link 51 | ``` 52 | 53 | - 2 - Then, move to `examples/api-server`: 54 | ``` 55 | examples/api-server$ npm link PATH/TO/flagship-js-sdk 56 | ``` 57 | 58 | ## Demo 59 | 60 | ### Customize your API responses 61 | 62 | Assuming you have a e-commerce website and you're looking to display some items on your home page. 63 | 64 | To fetch the items, you're currently calling `http://localhost:3000/items` 65 | 66 | ``` 67 | // api-server/routes/items.js 68 | 69 | router.get('/', function(req, res, next) { 70 | res.send(items); 71 | } 72 | ``` 73 | 74 | **Check the output:** 75 | 76 | ``` 77 | curl -X GET http://localhost:3000/items 78 | ``` 79 | 80 | > The output is an array of json (=item data) 81 | 82 | Now considering it's black friday, you're looking to target your users and provide a discount according to their purchasing frequency. Here comes Flagship SDK ~ ⛵️ 83 | 84 | In our example, we will specify it using a parameter: `discount`, which will give in our code: 85 | 86 | ``` 87 | // api-server/routes/items.js 88 | 89 | router.get('/', function(req, res, next) { 90 | if(req.query.discount === 'blackfriday') { 91 | // USE FLAGSHIP HERE ! 92 | } else { 93 | res.sendStatus(422); 94 | } 95 | } 96 | ``` 97 | 98 | The code which we need to write into the `if` condition is the following: 99 | 100 | ``` 101 | const visitorId= '134546'; 102 | const visitorContext= { 103 | // For the purpose of this example, let's generate a random context value 104 | buyerFrequency: Math.floor(Math.random() * (5) ), 105 | }; 106 | const activateAllModifications = true; 107 | 108 | // Create the flagship visitor with given context 109 | const fsVisitor = flagship.newVisitor(visitorId, visitorContext) 110 | 111 | // Wait initialization... 112 | fsVisitor.on('ready', () => { 113 | 114 | // Now extract desired modifications (in our case 'globalDiscount') + don't forget to specify the defaultValue 115 | fsVisitor.getModifications([ 116 | { 117 | key: 'globalDiscount', 118 | defaultValue: 0, 119 | } 120 | ], activateAllModifications) 121 | .then(({globalDiscount}) => { 122 | 123 | // Flagship returns the value specified on Flagship Dashboard according the visitor which we're targeting 124 | items.forEach(item => { 125 | item.discountPercentage = globalDiscount; 126 | }); 127 | 128 | // Send the items containing Flagship modifications 129 | res.send(items); 130 | }); 131 | }) 132 | ``` 133 | 134 | **Check the output:** 135 | 136 | ``` 137 | curl -X GET http://localhost:3000/items\?discount\=blackfriday 138 | ``` 139 | 140 | > The ouput is the same as previous response but the `discountPercentage` has been overridden for each item according to the data provided by Flagship
As you can see in the response, the overridden value is either `15` (if our visitor has less than 2 purchase frequency) or `30` (if more than 2) as we specified on the Flagship campaign 141 | 142 | ### Notify Flagship with a hit 143 | 144 | Following our `Customize your API responses` example, let's send a `transaction` if a user purchased an item. 145 | 146 | To do so, we defined a `POST` request: 147 | 148 | ``` 149 | // examples/api-server/routes/checkout.js 150 | 151 | router.post('/', function(req, res, next) { 152 | if (req.body && req.body.transactionId) { 153 | 154 | // USE FLAGSHIP HERE ! 155 | 156 | res.sendStatus(200); 157 | }); 158 | } else { 159 | res.sendStatus(422); 160 | } 161 | ``` 162 | 163 | The code which we need to write into the `if` condition is the following: 164 | 165 | ``` 166 | const visitorId = '134546'; 167 | const visitorContext = { 168 | buyerFrequency: Math.floor(Math.random() * 5) 169 | }; 170 | 171 | // Create the flagship visitor with given context 172 | const fsVisitor = flagship.newVisitor(visitorId, visitorContext); 173 | 174 | // Wait initialization... 175 | fsVisitor.on('ready', () => { 176 | // Now you can send the hit ! 177 | fsVisitor.sendHits([ 178 | { 179 | type: 'Transaction', 180 | data: { 181 | transactionId: req.body.transactionId, // required attribute 182 | affiliation: 'transaction' // required attribute 183 | // NOTE: value of 'affiliation' should match the KPI specified on Flagship Dashboard 184 | } 185 | } 186 | ]); 187 | ``` 188 | 189 | **Check the output:** 190 | Open your terminal, then execute: 191 | 192 | ``` 193 | curl -d "username=scott&password=secret&transactionId=12345" -X POST http://localhost:3000/checkout 194 | ``` 195 | 196 | ### Stress test / Ram performance 197 | 198 | Create a lot of visitor on your server, to see how it impacts the performance: 199 | 200 | ``` 201 | # creating 3 visitors: 202 | curl -d "nbVisitor=3" -X POST http://localhost:3000/fsVisitor/create 203 | ``` 204 | 205 | Search for a visitor with its id. 206 | 207 | ``` 208 | curl -X GET http://localhost:3000/getInfo\?id\=VISITOR_ID 209 | ``` 210 | 211 | Search for modifications assigned to a specific visitor. 212 | 213 | ``` 214 | curl -X GET http://localhost:3000/getModifications\?id\=VISITOR_ID 215 | ``` 216 | 217 | That's it ! 🎉 218 | 219 | Don't forget to have a look to the Flagship Dashboard screenshots of the use case which we used for those examples. 👇 👇 👇 220 | 221 | ## Flagship Dashboard screenshots 222 | 223 | > This is the screenshots of the use case which we used for this demo 224 | 225 | ### 1 - Summary 226 | 227 | ![Summary](https://storage.googleapis.com/flagship-dev-public-storage/Screenshot%20at%20Dec%2006%2015-08-55.png) 228 | 229 | ### 2 - Description 230 | 231 | ![Description](https://storage.googleapis.com/flagship-dev-public-storage/Screenshot%20at%20Dec%2006%2015-09-15.png) 232 | 233 | ### 3 - Cases 234 | 235 | ![Cases](https://storage.googleapis.com/flagship-dev-public-storage/Screenshot%20at%20Dec%2006%2015-09-29.png) 236 | 237 | ### 4 - Targeting 238 | 239 | ![Targeting](https://storage.googleapis.com/flagship-dev-public-storage/Screenshot%20at%20Dec%2006%2015-09-45.png) 240 | 241 | ## More about Flagship SDK ? 242 | 243 | [👉Click here 😎](../../README.md) 244 | 245 | ## What is Flagship ? ⛵️ 246 | 247 | [👉Click here 😄](https://www.abtasty.com/solutions-product-teams/) 248 | -------------------------------------------------------------------------------- /examples/api-server/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var cookieParser = require('cookie-parser'); 4 | var logger = require('morgan'); 5 | 6 | var indexRouter = require('./routes/index'); 7 | var itemsRouter = require('./routes/items'); 8 | var checkoutRouter = require('./routes/checkout'); 9 | var fsVisitorRouter = require('./routes/fsVisitor'); 10 | 11 | if (process.pid) { 12 | console.log('This process is your pid ' + process.pid); 13 | } 14 | 15 | var app = express(); 16 | 17 | app.set('view engine', 'html'); 18 | 19 | app.use(logger('dev')); 20 | app.use(express.json()); 21 | app.use(express.urlencoded({ extended: false })); 22 | app.use(cookieParser()); 23 | app.use(express.static(path.join(__dirname, 'public'))); 24 | 25 | app.use('/', indexRouter); 26 | app.use('/items', itemsRouter); 27 | app.use('/checkout', checkoutRouter); 28 | app.use('/fsVisitor', fsVisitorRouter); 29 | 30 | app.set('visitorList', []); 31 | 32 | module.exports = app; 33 | -------------------------------------------------------------------------------- /examples/api-server/bin/www.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var app = require('../app'); 6 | var debug = require('debug')('api-server:server'); 7 | var http = require('http'); 8 | 9 | /** 10 | * Get port from environment and store in Express. 11 | */ 12 | 13 | var port = normalizePort(process.env.PORT || '3000'); 14 | app.set('port', port); 15 | 16 | /** 17 | * Create HTTP server. 18 | */ 19 | 20 | var server = http.createServer(app); 21 | 22 | /** 23 | * Listen on provided port, on all network interfaces. 24 | */ 25 | 26 | server.listen(port); 27 | server.on('error', onError); 28 | server.on('listening', onListening); 29 | 30 | /** 31 | * Normalize a port into a number, string, or false. 32 | */ 33 | 34 | function normalizePort(val) { 35 | var port = parseInt(val, 10); 36 | 37 | if (isNaN(port)) { 38 | // named pipe 39 | return val; 40 | } 41 | 42 | if (port >= 0) { 43 | // port number 44 | return port; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * Event listener for HTTP server "error" event. 52 | */ 53 | 54 | function onError(error) { 55 | if (error.syscall !== 'listen') { 56 | throw error; 57 | } 58 | 59 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 60 | 61 | // handle specific listen errors with friendly messages 62 | switch (error.code) { 63 | case 'EACCES': 64 | console.error(bind + ' requires elevated privileges'); 65 | process.exit(1); 66 | break; 67 | case 'EADDRINUSE': 68 | console.error(bind + ' is already in use'); 69 | process.exit(1); 70 | break; 71 | default: 72 | throw error; 73 | } 74 | } 75 | 76 | /** 77 | * Event listener for HTTP server "listening" event. 78 | */ 79 | 80 | function onListening() { 81 | var addr = server.address(); 82 | var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 83 | debug('Listening on ' + bind); 84 | } 85 | -------------------------------------------------------------------------------- /examples/api-server/mock/items.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "id": "5dea2160130554576dd35ec8", 4 | "picture": "http://placehold.it/32x32", 5 | "currency": "$", 6 | "price": 135, 7 | "discountPercentage": 2, 8 | "color": "blue", 9 | "name": "mollit labore", 10 | "brand": "CALLFLEX", 11 | "description": "Reprehenderit minim in occaecat aliqua. Non pariatur adipisicing adipisicing eiusmod veniam aliquip duis nisi nostrud tempor ex sint. Esse dolor ullamco ad qui incididunt dolor veniam sint in sit exercitation ad. Veniam elit nisi anim consequat sit labore cupidatat aliqua. Exercitation et ullamco excepteur reprehenderit reprehenderit culpa duis aliqua ipsum quis ad anim. Do aliquip ullamco ea cillum officia qui. Anim sint commodo ut ex sint ea consectetur qui labore ipsum esse occaecat.\r\n", 12 | "creationDate": "2015-10-20T10:46:16 -02:00", 13 | "tags": [ 14 | "laboris", 15 | "ex", 16 | "consequat" 17 | ] 18 | }, 19 | { 20 | "id": "5dea216055d03e250fb45f58", 21 | "picture": "http://placehold.it/32x32", 22 | "currency": "$", 23 | "price": 320, 24 | "discountPercentage": 10, 25 | "color": "brown", 26 | "name": "aliquip tempor", 27 | "brand": "GENESYNK", 28 | "description": "Nulla duis id labore reprehenderit commodo elit cupidatat sint deserunt. Et incididunt eu esse quis dolor adipisicing qui deserunt duis deserunt mollit. Exercitation quis sunt laboris esse velit adipisicing. Ut irure est sint proident est.\r\n", 29 | "creationDate": "2016-10-17T03:09:30 -02:00", 30 | "tags": [ 31 | "ullamco", 32 | "consequat", 33 | "consequat" 34 | ] 35 | }, 36 | { 37 | "id": "5dea2160389ecf8bd25020f4", 38 | "picture": "http://placehold.it/32x32", 39 | "currency": "$", 40 | "price": 185, 41 | "discountPercentage": 0, 42 | "color": "brown", 43 | "name": "fugiat cupidatat", 44 | "brand": "FURNIGEER", 45 | "description": "Culpa reprehenderit sunt amet deserunt commodo. Sit enim voluptate nisi excepteur ut ea quis amet occaecat est ea. Quis dolor occaecat eiusmod est in cupidatat laborum labore amet laborum incididunt aliquip minim.\r\n", 46 | "creationDate": "2016-06-01T06:58:43 -02:00", 47 | "tags": [ 48 | "aute", 49 | "deserunt", 50 | "quis" 51 | ] 52 | }, 53 | { 54 | "id": "5dea21609ca016a84b9e7611", 55 | "picture": "http://placehold.it/32x32", 56 | "currency": "$", 57 | "price": 148, 58 | "discountPercentage": 9, 59 | "color": "green", 60 | "name": "ut anim", 61 | "brand": "ZIDOX", 62 | "description": "Id culpa adipisicing reprehenderit deserunt eiusmod eiusmod nulla sunt labore non cupidatat. Magna cupidatat aliqua commodo commodo adipisicing aute reprehenderit officia aliqua excepteur laboris eu esse aliquip. Enim nulla eiusmod culpa ea eu culpa ex exercitation ea laboris pariatur esse nulla anim.\r\n", 63 | "creationDate": "2016-10-11T08:18:28 -02:00", 64 | "tags": [ 65 | "nisi", 66 | "qui", 67 | "nostrud" 68 | ] 69 | }, 70 | { 71 | "id": "5dea21608dd9f021f6709fe9", 72 | "picture": "http://placehold.it/32x32", 73 | "currency": "$", 74 | "price": 340, 75 | "discountPercentage": 6, 76 | "color": "green", 77 | "name": "consequat consequat", 78 | "brand": "ZENTIX", 79 | "description": "Incididunt voluptate eu sunt minim aliqua laborum laborum deserunt esse esse adipisicing. Sit voluptate consectetur qui eiusmod exercitation elit minim commodo elit consequat amet enim. Duis deserunt cupidatat commodo tempor nostrud voluptate id consequat sint culpa anim eu ut. Exercitation voluptate ullamco cupidatat proident sit do aliqua cillum est nulla. Anim dolore officia amet do exercitation consectetur sunt culpa. Ea aute id voluptate ea ad sint voluptate anim.\r\n", 80 | "creationDate": "2017-11-15T05:33:36 -01:00", 81 | "tags": [ 82 | "fugiat", 83 | "veniam", 84 | "eiusmod" 85 | ] 86 | }, 87 | { 88 | "id": "5dea2160d78d59d16c6fae94", 89 | "picture": "http://placehold.it/32x32", 90 | "currency": "$", 91 | "price": 131, 92 | "discountPercentage": 0, 93 | "color": "brown", 94 | "name": "consequat ipsum", 95 | "brand": "LIQUICOM", 96 | "description": "Adipisicing consectetur eu mollit magna nulla. Voluptate irure amet ut aute voluptate nisi ad ipsum incididunt exercitation quis. Dolore sint sint dolor labore occaecat consectetur mollit deserunt culpa est est. Cupidatat ex esse proident id enim. Ullamco nulla aliqua officia minim deserunt exercitation excepteur cillum in irure voluptate laborum. Laborum ut nostrud exercitation ad quis consequat nisi.\r\n", 97 | "creationDate": "2016-03-10T11:14:17 -01:00", 98 | "tags": [ 99 | "sint", 100 | "consequat", 101 | "commodo" 102 | ] 103 | } 104 | ] -------------------------------------------------------------------------------- /examples/api-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www.js", 7 | "build": "npx webpack --config webpack.config.js", 8 | "start:mac": "DEBUG=api-server:* nodemon start", 9 | "start:linux": "DEBUG=api-server:* nodemon start", 10 | "start:windows": "set DEBUG=api-server:* & nodemon start" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Emilien Domenge-Heritier", 15 | "email": "emilien.domenge-heritier@abtasty.com", 16 | "url": "https://domenge.fr" 17 | }, 18 | { 19 | "name": "Guillaume Jacquart", 20 | "email": "guillaume.jacquart@abtasty.com" 21 | } 22 | ], 23 | "dependencies": { 24 | "@flagship.io/js-sdk": "^2.2.2", 25 | "cookie-parser": "~1.4.4", 26 | "debug": "~2.6.9", 27 | "express": "~4.16.1", 28 | "morgan": "~1.9.1", 29 | "webpack-node-externals": "^2.5.0" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^2.0.1", 33 | "webpack": "^4.44.1", 34 | "webpack-cli": "^3.3.12" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/api-server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Express 5 | 6 | 7 | 8 | 9 |

Express

10 |

Welcome to Express

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/api-server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /examples/api-server/routes/checkout.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var flagship = require('./../services/flagship'); 4 | 5 | router.post('/', function(req, res, next) { 6 | console.log('Got body:', req.body); 7 | if (req.body && req.body.transactionId) { 8 | const visitorId = '134546'; // mock visitorId (normally should be taken from DB, ...) 9 | const visitorContext = { 10 | buyerFrequency: Math.floor(Math.random() * 5) 11 | }; 12 | const fsVisitor = flagship.newVisitor(visitorId, visitorContext); 13 | 14 | fsVisitor.on('ready', () => { 15 | console.log('fsVisitor.context', fsVisitor.context); 16 | fsVisitor.sendHits([ 17 | { 18 | type: 'Transaction', 19 | data: { 20 | transactionId: req.body.transactionId, 21 | affiliation: 'transaction' 22 | // totalRevenue: req.body.totalRevenue, 23 | // shippingCost: req.body.shippingCost, 24 | // shippingMethod: req.body.shippingMethod, 25 | // currency: req.body.currency, 26 | // taxes: req.body.taxes, 27 | // paymentMethod:req.body.paymentMethod, 28 | // itemCount: req.body.itemCount, 29 | // couponCode: req.body.couponCode, 30 | // documentLocation: req.body.documentLocation, 31 | // pageTitle: req.body.pageTitle 32 | } 33 | } 34 | ]); 35 | res.sendStatus(200); 36 | }); 37 | } else { 38 | res.sendStatus(422); 39 | } 40 | }); 41 | 42 | module.exports = router; 43 | -------------------------------------------------------------------------------- /examples/api-server/routes/fsVisitor.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var flagship = require('../services/flagship'); 4 | const bodyParser = require('body-parser').json(); 5 | 6 | const createUUID = function () { 7 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 8 | var r = (Math.random() * 16) | 0, 9 | v = c == 'x' ? r : (r & 0x3) | 0x8; 10 | return v.toString(16); 11 | }); 12 | }; 13 | 14 | const createVisitor = function (req, nbVisitorLeftToCreate, finishCallback) { 15 | const visitorId = createUUID(); // mock visitorId (normally should be taken from DB, ...) 16 | const visitorContext = { 17 | buyerFrequency: Math.floor(Math.random() * 5) 18 | }; 19 | const fsVisitor = flagship.newVisitor(visitorId, visitorContext); 20 | 21 | fsVisitor.on('ready', () => { 22 | var visitorList = req.app.get('visitorList'); 23 | visitorList.push(fsVisitor); 24 | req.app.set('visitorList', visitorList); 25 | 26 | console.log('fsVisitor (id=' + visitorId + ') is ready'); 27 | 28 | if (nbVisitorLeftToCreate - 1 === 0) { 29 | finishCallback(visitorList); 30 | } else { 31 | createVisitor(req, nbVisitorLeftToCreate - 1, finishCallback); 32 | } 33 | }); 34 | }; 35 | 36 | router.post('/create', bodyParser, function (req, res, next) { 37 | console.log('Got body:', req.body); 38 | if (req.body && req.body.nbVisitor) { 39 | createVisitor(req, req.body.nbVisitor, function (visitors) { 40 | res.status(200).send({ 41 | totalVisitor: visitors.length, 42 | visitorIdList: visitors.map((v) => v.id) 43 | }); 44 | }); 45 | } else { 46 | res.sendStatus(422); 47 | } 48 | }); 49 | 50 | router.get('/getInfo', function (req, res, next) { 51 | const visitorId = req.query.id; 52 | if (visitorId) { 53 | var visitorList = req.app.get('visitorList'); 54 | var visitor = visitorList.filter((v) => v.id === visitorId); 55 | if (visitor.length === 0) { 56 | res.sendStatus(404); 57 | } else { 58 | res.status(200).send(visitor[0]); 59 | } 60 | } else { 61 | res.sendStatus(422); 62 | } 63 | }); 64 | 65 | router.get('/getModifications', function (req, res, next) { 66 | const visitorId = req.query.id; 67 | if (visitorId) { 68 | var visitorList = req.app.get('visitorList'); 69 | var visitor = visitorList.filter((v) => v.id === visitorId); 70 | if (visitor.length === 0) { 71 | res.status(404).send('visitor not found'); 72 | } else { 73 | visitor[0] 74 | .getAllModifications() 75 | .then((response) => { 76 | res.status(200).send(response.data.campaigns); 77 | }) 78 | .catch((err) => res.status(400).send(err.stack)); 79 | } 80 | } else { 81 | res.sendStatus(422); 82 | } 83 | }); 84 | 85 | router.get('/empty', function (req, res, next) { 86 | req.app.set('visitorList', []); 87 | res.sendStatus(200); 88 | }); 89 | 90 | module.exports = router; 91 | -------------------------------------------------------------------------------- /examples/api-server/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /examples/api-server/routes/items.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var mock = require('./../mock/items'); 4 | var flagship = require('./../services/flagship'); 5 | 6 | /* GET users listing. */ 7 | router.get('/', function (req, res, next) { 8 | const items = mock; 9 | if (req.query.discount === 'blackfriday') { 10 | console.log('Black friday discount detected'); 11 | const visitorId = '134546'; // mock visitorId (normally should be taken from DB, ...) 12 | const visitorContext = { 13 | buyerFrequency: Math.floor(Math.random() * 5), 14 | }; 15 | const activateAllModifications = true; 16 | const fsVisitor = flagship.newVisitor(visitorId, visitorContext); 17 | 18 | fsVisitor.on('ready', () => { 19 | console.log('fsVisitor.context', fsVisitor.context); 20 | const { globalDiscount } = fsVisitor.getModifications( 21 | [ 22 | { 23 | key: 'globalDiscount', 24 | defaultValue: 0, 25 | }, 26 | ], 27 | activateAllModifications 28 | ); 29 | items.forEach((item) => { 30 | item.discountPercentage = globalDiscount; 31 | }); 32 | res.send(items); 33 | }); 34 | } else { 35 | res.send(items); 36 | } 37 | }); 38 | 39 | module.exports = router; 40 | -------------------------------------------------------------------------------- /examples/api-server/services/flagship.js: -------------------------------------------------------------------------------- 1 | var flagship = require('@flagship.io/js-sdk'); 2 | 3 | const sdk = flagship.start('bn1ab7m56qolupi5sa0g', 'j2jL0rzlgVaODLw2Cl4JC3f4MflKrMgIaQOENv36', { 4 | fetchNow: true, 5 | enableConsoleLogs: true, 6 | // decisionMode: 'Bucketing', // Uncomment this line to enable "Bucketing" mode 7 | // pollingInterval: 1, // Uncomment this line to do a bucketing polling every minutes 8 | activateNow: true 9 | }); 10 | 11 | module.exports = sdk; 12 | -------------------------------------------------------------------------------- /examples/api-server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | entry: { server: './bin/www.js' }, 6 | mode: 'production', 7 | target: 'node', 8 | node: { 9 | fs: 'empty' 10 | }, 11 | node: { 12 | // Need this when working with express, otherwise the build fails 13 | __dirname: false, // if you don't put this is, __dirname 14 | __filename: false // and __filename return blank or / 15 | }, 16 | externals: [nodeExternals()], 17 | output: { 18 | filename: 'main.js', 19 | path: path.resolve(__dirname, 'dist') 20 | }, 21 | resolve: { 22 | modules: ['node_modules'], 23 | extensions: ['.js', '.jsx'] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /examples/react-app/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/react-app/.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/* -------------------------------------------------------------------------------- /examples/react-app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "extends": [ 7 | "plugin:@typescript-eslint/recommended", 8 | "airbnb", 9 | "prettier", 10 | "prettier/react", 11 | "eslint:recommended", 12 | "plugin:react/recommended", 13 | "plugin:jsx-a11y/recommended" 14 | ], 15 | "env": { 16 | "browser": true 17 | }, 18 | "rules": { 19 | "react/jsx-filename-extension": [0, { "extensions": [".js", ".jsx"] }], 20 | "import/no-extraneous-dependencies": "off", 21 | "jsx-a11y/anchor-has-content": "off", 22 | "jsx-a11y/anchor-is-valid": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/react-app/README.md: -------------------------------------------------------------------------------- 1 | ![Flagship logo](../../src/assets/img/flagshipLogo.jpg) 2 | 3 | # Sample of React app using [Flagship - JS SDK](../../README.md) 4 | 5 | ## Online demo 6 | 7 | 8 | 9 | ## Try it locally 10 | 11 | ### Prerequisites 12 | 13 | - **Node.js**: version 6.0.0 or later... 14 | 15 | - **Npm**: version 3.0.0 or later... 16 | 17 | ## Getting Started 18 | 19 | - **Install** the node module: 20 | 21 | ``` 22 | examples/react-app$ npm install 23 | ``` 24 | 25 | - **Start** the project: 26 | 27 | ``` 28 | examples/react-app$ npm start 29 | ``` 30 | 31 | ## Run with local Flagship JS SDK 32 | 33 | - You need to link `@flagship.io/js-sdk` : 34 | 35 | - 1 - At the root level (=`PATH/TO/flagship-js-sdk`), run: 36 | 37 | ``` 38 | flagship-js-sdk$ npm link 39 | ``` 40 | 41 | - 2 - Then, move to `examples/react-app`: 42 | ``` 43 | examples/react-app$ npm link PATH/TO/flagship-js-sdk 44 | ``` 45 | 46 | ## More about Flagship SDK ? 47 | 48 | [👉Click here 😎](../../README.md) 49 | 50 | ## What is Flagship ? ⛵️ 51 | 52 | [👉Click here 😄](https://www.abtasty.com/solutions-product-teams/) 53 | -------------------------------------------------------------------------------- /examples/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": false, 5 | "homepage": "https://abtasty.github.io/flagship-js-sdk/", 6 | "dependencies": { 7 | "@flagship.io/js-sdk": "^2.2.2", 8 | "@tenon-io/tenon-codeblock": "^1.0.0", 9 | "@types/node": "12.12.14", 10 | "@types/react": "16.9.13", 11 | "@types/react-dom": "16.9.4", 12 | "bootstrap": "^4.4.1", 13 | "gh-pages": "^2.1.1", 14 | "react": "^16.12.0", 15 | "react-bootstrap": "^1.0.0-beta.16", 16 | "react-dom": "^16.12.0", 17 | "react-scripts": "3.2.0", 18 | "typescript": "3.7.2" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "deploy:github": "npm run build && gh-pages -d build", 24 | "test": "react-scripts test --watchAll=false", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "contributors": [ 43 | { 44 | "name": "Emilien Domenge-Heritier", 45 | "email": "emilien.domenge-heritier@abtasty.com", 46 | "url": "https://domenge.fr" 47 | }, 48 | { 49 | "name": "Guillaume Jacquart", 50 | "email": "guillaume.jacquart@abtasty.com" 51 | } 52 | ], 53 | "devDependencies": { 54 | "@types/enzyme": "^3.10.3", 55 | "@types/jest": "^24.0.23", 56 | "@typescript-eslint/eslint-plugin": "^2.9.0", 57 | "@typescript-eslint/parser": "^2.9.0", 58 | "enzyme": "^3.10.0", 59 | "enzyme-adapter-react-16": "^1.15.1", 60 | "eslint": "^6.7.1", 61 | "eslint-config-airbnb": "^18.0.1", 62 | "eslint-config-prettier": "^6.10.1", 63 | "eslint-plugin-import": "^2.18.2", 64 | "eslint-plugin-jsx-a11y": "^6.2.3", 65 | "eslint-plugin-react": "^7.16.0", 66 | "eslint-plugin-react-hooks": "^1.7.0", 67 | "jest": "^24.9.0", 68 | "react-test-renderer": "^16.12.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/examples/react-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | 30 | Flagship - React SDK 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Flagship - React SDK", 3 | "name": "Official Flagship - React SDK example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /examples/react-app/src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Work+Sans&display=swap'); 2 | body { 3 | font-family: 'Work Sans'; 4 | } 5 | 6 | html { 7 | scroll-behavior: smooth; 8 | } 9 | 10 | .logoAdjust { 11 | height: 30px; 12 | padding-right: 15px; 13 | } 14 | 15 | .bfLightBtn { 16 | color: black !important; 17 | background-color: white !important; 18 | border-color: white !important; 19 | } 20 | 21 | .bfDarkBtn { 22 | color: white !important; 23 | background-color: black !important; 24 | border-color: black !important; 25 | } 26 | 27 | .fsNavbar, 28 | .alert-dark.fs-alert { 29 | background-color: #343434; 30 | } 31 | 32 | .modal-content { 33 | background-color: #343434 !important; 34 | color: white !important; 35 | } 36 | 37 | .modal-header, 38 | .modal-footer { 39 | border: none !important; 40 | } 41 | 42 | .fsNavbar .nav-link, 43 | .fsNavbar .navbar-brand, 44 | .modal-content a { 45 | color: #ffffff !important; 46 | } 47 | 48 | .fsContainer { 49 | background-color: #272727; 50 | padding-top: 55px; 51 | } 52 | 53 | .alert-dark.fs-alert { 54 | color: #ffffff; 55 | border-color: #343434; 56 | border-radius: 4px; 57 | } 58 | 59 | .alert-info.fs-alert { 60 | color: #ffffff; 61 | border-radius: 4px; 62 | border: solid 1px #4f4f4f; 63 | background-color: #343434; 64 | } 65 | 66 | .fsAnchor { 67 | display: block; 68 | position: relative; 69 | top: -80px; 70 | visibility: hidden; 71 | } 72 | -------------------------------------------------------------------------------- /examples/react-app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import flagship from '@flagship.io/js-sdk'; 2 | import Enzyme from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { EventEmitter } from 'events'; 5 | import React from 'react'; 6 | import App from './App'; 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | 10 | flagship.start = jest.fn().mockImplementation(() => ({ 11 | newVisitor: () => { 12 | const self = new EventEmitter(); 13 | return self; 14 | } 15 | })); 16 | 17 | describe('Flagship - React SDK', () => { 18 | let wrapper; 19 | const setFsVisitor = jest.fn(); 20 | const useStateSpy: jest.SpyInstance = jest.spyOn(React, 'useState'); 21 | useStateSpy.mockImplementation((fsVisitor) => [fsVisitor, setFsVisitor]); 22 | 23 | beforeEach(() => { 24 | wrapper = Enzyme.mount(); 25 | }); 26 | 27 | afterEach(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | it('renders without crashing', () => { 32 | // console.log(wrapper.debug()); 33 | expect(flagship.start).toBeCalledTimes(1); 34 | expect(setFsVisitor).toHaveBeenCalledTimes(1); 35 | expect(wrapper.find('Alert').length).toBe(10); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/react-app/src/assets/Flagship-horizontal-product-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/examples/react-app/src/assets/Flagship-horizontal-product-white.png -------------------------------------------------------------------------------- /examples/react-app/src/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navbar, Nav, Form } from 'react-bootstrap'; 3 | import Logo from '../../assets/Flagship-horizontal-product-white.png'; 4 | 5 | const Header: React.FC = () => ( 6 | <> 7 | 8 | 12 | Logo Flagship 17 | JS SDK with React 18 | 19 | 26 | 27 |
28 | 29 | Github 30 | 31 | 32 | What is Flagship ? 33 | 34 |
35 |
36 | 37 | ); 38 | export default Header; 39 | -------------------------------------------------------------------------------- /examples/react-app/src/components/modal/howToCreateToggle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Modal, Image } from 'react-bootstrap'; 3 | 4 | const HowToCreateToggleModal: React.FC = () => { 5 | const [show, setShow] = useState(false); 6 | 7 | const handleClose = () => setShow(false); 8 | const handleShow = () => setShow(true); 9 | 10 | return ( 11 | <> 12 | 15 | 16 | 17 | 18 | Create a use case on Flagship Dashboard 19 | 20 | 21 |
1 - Sign in on Flagship
22 |

23 | {' '} 24 | 29 | 👉 Click here to log in 30 | 31 |

32 |
2 - Click on "+" button
33 | 39 | 43 | 44 |
3 - Select or create a new project
45 | 51 | 55 | 56 |
4 - Name your project (if creating a new one)
57 | 63 | 67 | 68 |
5 - Choose a use case
69 | 75 | 79 | 80 |
6 - Fill use case's basic info
81 | 87 | 91 | 92 |
7 - Define use case's flag and scenarios
93 | 99 | 103 | 104 |
8 - Define use case's targeting
105 | 111 | 115 | 116 |

All done 👏👏

117 | 123 |
124 | 125 | 128 | 129 |
130 | 131 | ); 132 | }; 133 | 134 | export default HowToCreateToggleModal; 135 | -------------------------------------------------------------------------------- /examples/react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .h1, .h2, .h3, .h4, .h5, .h6 { 16 | height: unset; 17 | } -------------------------------------------------------------------------------- /examples/react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import 'bootstrap/dist/css/bootstrap.min.css'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: https://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /examples/react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-app/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /examples/react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": false, 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flagship.io/js-sdk", 3 | "version": "2.2.13", 4 | "description": "Flagship JS SDK", 5 | "main": "dist/index.node.js", 6 | "browser": "dist/index.browser.js", 7 | "react-native": "dist/index.reactNative.js", 8 | "files": [ 9 | "dist", 10 | "public", 11 | "CONTRIBUTING.md", 12 | "README.md", 13 | "package.json" 14 | ], 15 | "types": "dist/src/index.d.ts", 16 | "engines": { 17 | "npm": ">=3", 18 | "node": ">=6" 19 | }, 20 | "scripts": { 21 | "test": "jest --verbose", 22 | "test:coverage": "jest --coverage", 23 | "test:badges": "npm run test:coverage && jest-coverage-badges && cp ./coverage/badge-lines.svg ./badges/ && cp ./coverage/badge-functions.svg ./badges/", 24 | "test:react-sample": "cd ./examples/react-app && npm test", 25 | "eslint": "eslint --version && eslint --quiet .ts,.js", 26 | "release": "np", 27 | "version": "npm run build", 28 | "publish": "npm run test:badges && npm run update:react-app && npm run update:api-server && git add . && git commit -m\"Post publish update\" && git push && npm run publish:react", 29 | "publish:react": "cd ./examples/react-app && npm run deploy:github", 30 | "update:react-app": "cd ./examples/react-app && npm install @flagship.io/js-sdk@latest --save && cd ../../", 31 | "update:api-server": "cd ./examples/api-server && npm install @flagship.io/js-sdk@latest --save && cd ../../", 32 | "update:fsLog": "npm install @flagship.io/js-sdk-logs@latest --save", 33 | "contributors:add": "all-contributors add", 34 | "contributors:generate": "all-contributors generate", 35 | "stats:dev": "rm -rf dist && webpack --env=stats", 36 | "stats:prod": "webpack --profile --json --env=prod > stats.json && webpack-bundle-analyzer ./stats.json", 37 | "build": "rm -rf dist && webpack --env=prod && ./cleanDts.sh" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/abtasty/flagship-js-sdk" 42 | }, 43 | "license": "Apache-2.0", 44 | "bugs": { 45 | "url": "https://github.com/abtasty/flagship-js-sdk/issues", 46 | "email": "emilien.domenge-heritier@abtasty.com" 47 | }, 48 | "contributors": [ 49 | { 50 | "name": "Emilien Domenge-Heritier", 51 | "email": "emilien.domenge-heritier@abtasty.com", 52 | "url": "https://domenge.fr" 53 | }, 54 | { 55 | "name": "Guillaume Jacquart", 56 | "email": "guillaume.jacquart@abtasty.com" 57 | }, 58 | { 59 | "name": "Yanis Tam", 60 | "email": "yanis@abtasty.com" 61 | } 62 | ], 63 | "keywords": [ 64 | "flagship", 65 | "abtasty", 66 | "node", 67 | "sdk" 68 | ], 69 | "homepage": "https://github.com/abtasty/flagship-js-sdk", 70 | "devDependencies": { 71 | "@babel/core": "^7.7.2", 72 | "@babel/preset-env": "^7.7.1", 73 | "@babel/preset-typescript": "^7.7.2", 74 | "@types/jest": "^24.0.22", 75 | "@types/node": "^12.12.7", 76 | "@typescript-eslint/eslint-plugin": "^2.8.0", 77 | "@typescript-eslint/parser": "^2.8.0", 78 | "all-contributors-cli": "^6.16.1", 79 | "babel-jest": "^24.9.0", 80 | "babel-loader": "^8.0.6", 81 | "dts-bundle-generator": "^4.3.0", 82 | "eslint": "^6.1.0", 83 | "eslint-config-airbnb-base": "^14.0.0", 84 | "eslint-config-prettier": "^6.11.0", 85 | "eslint-loader": "^3.0.3", 86 | "eslint-plugin-import": "^2.18.2", 87 | "eslint-plugin-prettier": "^3.1.4", 88 | "jest": "^24.9.0", 89 | "jest-coverage-badges": "^1.1.2", 90 | "jest-localstorage-mock": "^2.4.3", 91 | "jest-mock-axios": "^3.1.2", 92 | "np": "^6.2.3", 93 | "prettier": "2.0.4", 94 | "source-map-loader": "^0.2.4", 95 | "supertest": "^4.0.2", 96 | "ts-loader": "^7.0.5", 97 | "typescript": "^3.7.2", 98 | "webpack-bundle-analyzer": "^3.8.0", 99 | "webpack": "^5.66.0", 100 | "webpack-cli": "^4.9.1", 101 | "webpack-merge": "^5.8.0", 102 | "webpack-node-externals": "^3.0.0" 103 | }, 104 | "dependencies": { 105 | "@flagship.io/js-sdk-logs": "^0.1.4", 106 | "axios": "^0.21.1", 107 | "events": "^3.1.0", 108 | "react-native-murmurhash": "^1.1.0", 109 | "validate.js": "^0.13.1" 110 | }, 111 | "jest": { 112 | "setupFiles": [ 113 | "jest-localstorage-mock" 114 | ], 115 | "testEnvironment": "node", 116 | "coverageReporters": [ 117 | "json-summary", 118 | "text", 119 | "lcov" 120 | ], 121 | "collectCoverage": true, 122 | "transform": { 123 | "^.+\\.[t|j]sx?$": "babel-jest" 124 | }, 125 | "coveragePathIgnorePatterns": [ 126 | "/test/", 127 | "/node_modules/" 128 | ], 129 | "testPathIgnorePatterns": [ 130 | "/src/config/", 131 | "/examples/" 132 | ] 133 | } 134 | } -------------------------------------------------------------------------------- /src/assets/img/flagshipLogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flagship-io/flagship-js-sdk/5395d29441487f76eec9e57bafb7f31e711ec38c/src/assets/img/flagshipLogo.jpg -------------------------------------------------------------------------------- /src/class/bucketing/types.ts: -------------------------------------------------------------------------------- 1 | export type BucketingApiResponse = { 2 | campaigns: BucketingCampaign[]; 3 | panic: boolean; 4 | lastModifiedDate: string; 5 | }; 6 | 7 | export type BucketingCampaign = { 8 | id: string; 9 | type: string; 10 | variationGroups: BucketingVariationGroups[]; 11 | }; 12 | 13 | export type BucketingVariationGroups = { 14 | id: string; 15 | targeting: { 16 | targetingGroups: BucketingTargetingGroups[]; 17 | }; 18 | variations: BucketingVariation[]; 19 | }; 20 | 21 | export type BucketingVariation = { 22 | id: string; 23 | modifications: { 24 | type: string; 25 | value: { [key: string]: null | string }; 26 | }; 27 | allocation: number; 28 | reference: boolean; 29 | }; 30 | 31 | export type BucketingTargetingGroups = { 32 | targetings: BucketingTargetings[]; 33 | }; 34 | 35 | export type BucketingTargetings = { 36 | operator: BucketingOperator; 37 | key: string | 'fs_all_users' | 'fs_users'; 38 | value: BucketingTypes; 39 | }; 40 | 41 | export type BucketingOperator = 42 | | 'EQUALS' 43 | | 'NOT_EQUALS' 44 | | 'LOWER_THAN' 45 | | 'LOWER_THAN_OR_EQUALS' 46 | | 'GREATER_THAN' 47 | | 'GREATER_THAN_OR_EQUALS' 48 | | 'STARTS_WITH' 49 | | 'ENDS_WITH' 50 | | 'CONTAINS' 51 | | 'NOT_CONTAINS'; 52 | 53 | export type BucketingTypes = string | string[] | number | number[] | boolean | boolean[]; 54 | -------------------------------------------------------------------------------- /src/class/bucketingVisitor/types.ts: -------------------------------------------------------------------------------- 1 | export type CheckAssertionOptions = { 2 | shouldHaveAllAssertionsValid? : boolean; 3 | } -------------------------------------------------------------------------------- /src/class/cacheManager/clientCacheManager.ts: -------------------------------------------------------------------------------- 1 | import { IFsCacheManager } from '../../types'; 2 | 3 | export const CLIENT_CACHE_KEY = 'FS_CLIENT_VISITOR'; 4 | 5 | const clientCacheManager: IFsCacheManager = { 6 | saveVisitorProfile: (visitorId, visitorProfile) => { 7 | try { 8 | localStorage.setItem(CLIENT_CACHE_KEY, JSON.stringify(visitorProfile)); 9 | } catch {} 10 | }, 11 | loadVisitorProfile: (visitorId) => { 12 | let data; 13 | try { 14 | data = localStorage.getItem(CLIENT_CACHE_KEY); 15 | } catch { 16 | data = null; 17 | } 18 | return data ? JSON.parse(data) : null; 19 | } 20 | }; 21 | 22 | export default clientCacheManager; 23 | -------------------------------------------------------------------------------- /src/class/flagshipVisitor/types.ts: -------------------------------------------------------------------------------- 1 | export type FlagshipModification = string | boolean | number | null | Array | { [key: string]: any }; 2 | 3 | export type FlagshipVisitorContext = { 4 | [key: string]: boolean | number | string; 5 | }; 6 | 7 | export type FsModifsRequestedList = Array<{ 8 | key: string; 9 | defaultValue: FlagshipModification; 10 | activate?: boolean; 11 | }>; 12 | 13 | export type DecisionApiResponse = { 14 | data: DecisionApiResponseData; 15 | status: number; 16 | }; 17 | 18 | export type DecisionApiSimpleResponse = { 19 | [key: string]: FlagshipModification; 20 | }; 21 | 22 | export type DecisionApiResponseData = { 23 | visitorId: string; 24 | campaigns: DecisionApiCampaign[]; 25 | panic?: boolean; 26 | }; 27 | 28 | export type GetModificationInfoOutput = { 29 | campaignId: string; 30 | isReference: boolean; 31 | variationId: string; 32 | variationGroupId: string; 33 | }; 34 | 35 | export type GetModificationsOutput = { 36 | [key: string]: FlagshipModification; 37 | }; 38 | 39 | export type AuthenticateVisitorOutput = Promise; 40 | 41 | export type UnauthenticateVisitorOutput = Promise; 42 | 43 | export type checkCampaignsActivatedMultipleTimesOutput = { 44 | activateCampaign: { 45 | [key: string]: { 46 | directActivate: Array; 47 | indirectActivate: Array; 48 | }; 49 | }; 50 | activateKey: { [key: string]: number }; 51 | }; 52 | 53 | export type DecisionApiResponseDataSimpleComputed = { 54 | [key: string]: FlagshipModification; 55 | }; 56 | 57 | export type DecisionApiCampaign = { 58 | id: string; 59 | variationGroupId: string; 60 | variation: { 61 | id: string; 62 | reference?: boolean; 63 | modifications: { 64 | type: string; 65 | value: { 66 | [key: string]: FlagshipModification; 67 | }; 68 | }; 69 | }; 70 | }; 71 | 72 | export type DecisionApiResponseDataFullComputed = { 73 | [key: string]: { 74 | value: Array; 75 | type: Array; 76 | campaignId: Array; 77 | variationId: Array; 78 | variationGroupId: Array; 79 | isRequested: boolean; 80 | isActivateNeeded: boolean; 81 | }; 82 | }; 83 | 84 | export type ActivatedArchived = { 85 | variationId: Array; 86 | variationGroupId: Array; 87 | }; 88 | 89 | export type ModificationsInternalStatus = { 90 | [key: string]: { 91 | value: Array; 92 | type: Array; 93 | campaignId: Array; 94 | variationId: Array; 95 | variationGroupId: Array; 96 | activated: ActivatedArchived; 97 | }; 98 | }; 99 | 100 | export type HitShape = 101 | | { type: 'Screen'; data: ScreenViewHit } 102 | | { type: 'ScreenView'; data: ScreenViewHit } // Deprecated type: 'ScreenView' 103 | | { type: 'Page'; data: PageViewHit } 104 | | { type: 'PageView'; data: PageViewHit } // Deprecated type: 'PageView' 105 | | { type: 'Transaction'; data: TransactionHit } 106 | | { type: 'Item'; data: ItemHit } 107 | | { type: 'Event'; data: EventHit }; 108 | 109 | export type TransactionHit = CommonHit & { 110 | transactionId: string; 111 | affiliation: string; 112 | totalRevenue?: number; 113 | shippingCost?: number; 114 | shippingMethod?: string; 115 | taxes?: number; 116 | currency?: string; 117 | paymentMethod?: string; 118 | itemCount?: number; 119 | couponCode?: string; 120 | documentLocation?: string; 121 | pageTitle?: string; 122 | }; 123 | 124 | export type ItemHit = CommonHit & { 125 | transactionId: string; 126 | name: string; 127 | price?: number; 128 | code?: string; 129 | category?: string; 130 | quantity?: number; 131 | documentLocation?: string; 132 | pageTitle?: string; 133 | }; 134 | 135 | export type EventHit = CommonHit & { 136 | category: 'Action Tracking' | 'User Engagement'; 137 | action: string; 138 | label?: string; 139 | value?: number; 140 | documentLocation?: string; 141 | pageTitle?: string; 142 | }; 143 | 144 | export type ScreenViewHit = CommonHit & { 145 | documentLocation: string; 146 | pageTitle: string; 147 | }; 148 | 149 | export type PageViewHit = CommonHit & { 150 | documentLocation: string; 151 | pageTitle: string; 152 | }; 153 | 154 | export type CommonHit = { 155 | protocolVersion?: string; 156 | userIp?: string; 157 | documentReferrer?: string; 158 | viewportSize?: string; 159 | screenResolution?: string; 160 | documentEncoding?: string; 161 | screenColorDepth?: string; 162 | userLanguage?: string; 163 | javaEnabled?: string; 164 | flashVersion?: string; 165 | queueTime?: string; 166 | currentSessionTimeStamp?: string; 167 | sessionNumber?: string; 168 | }; 169 | -------------------------------------------------------------------------------- /src/class/panicMode/panicMode.test.ts: -------------------------------------------------------------------------------- 1 | import PanicMode from './panicMode'; 2 | import testConfig from '../../config/test'; 3 | import { IFsPanicMode } from '../../types'; 4 | 5 | let panicModeInstance: IFsPanicMode = null; 6 | 7 | let spyWarnLogs; 8 | let spyErrorLogs; 9 | let spyFatalLogs; 10 | let spyInfoLogs; 11 | let spyDebugLogs; 12 | 13 | type initSpyLogsOutput = { 14 | spyWarnLogs: jest.SpyInstance; 15 | spyErrorLogs: jest.SpyInstance; 16 | spyFatalLogs: jest.SpyInstance; 17 | spyInfoLogs: jest.SpyInstance; 18 | spyDebugLogs: jest.SpyInstance; 19 | }; 20 | 21 | const initSpyLogs = (vInstance): initSpyLogsOutput => { 22 | spyFatalLogs = jest.spyOn(vInstance.log, 'fatal'); 23 | spyWarnLogs = jest.spyOn(vInstance.log, 'warn'); 24 | spyInfoLogs = jest.spyOn(vInstance.log, 'info'); 25 | spyDebugLogs = jest.spyOn(vInstance.log, 'debug'); 26 | spyErrorLogs = jest.spyOn(vInstance.log, 'error'); 27 | 28 | return { 29 | spyWarnLogs, 30 | spyErrorLogs, 31 | spyFatalLogs, 32 | spyInfoLogs, 33 | spyDebugLogs 34 | }; 35 | }; 36 | 37 | const testConfigWithoutFetchNow = { ...testConfig, fetchNow: false }; 38 | 39 | describe('PanicMode', () => { 40 | beforeAll(() => { 41 | // nothing 42 | }); 43 | afterEach(() => { 44 | panicModeInstance = null; 45 | }); 46 | it('should warn a debug log if setting panic mode twice in a row', () => { 47 | panicModeInstance = new PanicMode(testConfigWithoutFetchNow); 48 | initSpyLogs(panicModeInstance); 49 | 50 | panicModeInstance.setPanicModeTo(true); 51 | 52 | expect(spyDebugLogs).toHaveBeenCalledTimes(0); 53 | expect(spyInfoLogs).toHaveBeenCalledTimes(1); 54 | expect(spyErrorLogs).toHaveBeenCalledTimes(0); 55 | expect(spyFatalLogs).toHaveBeenCalledTimes(0); 56 | expect(spyWarnLogs).toHaveBeenCalledTimes(0); 57 | 58 | expect(spyInfoLogs).toHaveBeenNthCalledWith(1, 'panic mode is ENABLED. SDK will turn into safe mode.'); 59 | 60 | panicModeInstance.setPanicModeTo(true); 61 | 62 | expect(spyDebugLogs).toHaveBeenCalledTimes(1); 63 | expect(spyInfoLogs).toHaveBeenCalledTimes(1); 64 | expect(spyErrorLogs).toHaveBeenCalledTimes(0); 65 | expect(spyFatalLogs).toHaveBeenCalledTimes(0); 66 | expect(spyWarnLogs).toHaveBeenCalledTimes(0); 67 | 68 | const strDate: string = new Date().toDateString(); 69 | expect(spyDebugLogs).toHaveBeenNthCalledWith(1, `panic mode already ENABLED since ${strDate}`); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/class/panicMode/panicMode.ts: -------------------------------------------------------------------------------- 1 | import FlagshipLogger, { FsLogger } from '@flagship.io/js-sdk-logs'; 2 | import { IFsPanicMode, FlagshipSdkConfig } from '../../types'; 3 | import { DecisionApiResponseData } from '../flagshipVisitor/types'; 4 | import { BucketingApiResponse } from '../bucketing/types'; 5 | import { SetPanicModeToOptions } from './types'; 6 | 7 | class PanicMode implements IFsPanicMode { 8 | enabled: boolean; 9 | 10 | beginDate: Date | null; 11 | 12 | log: FsLogger; 13 | 14 | constructor(config: FlagshipSdkConfig) { 15 | this.enabled = false; 16 | this.beginDate = null; 17 | this.log = FlagshipLogger.getLogger(config, `Flagship SDK - panic mode`); 18 | } 19 | 20 | public setPanicModeTo(value: boolean, options: SetPanicModeToOptions = { sendLogs: true }): void { 21 | const { sendLogs } = options; 22 | if (value === this.enabled) { 23 | if (sendLogs) { 24 | this.log.debug( 25 | value ? `panic mode already ENABLED since ${this.beginDate.toDateString()}` : 'panic mode already DISABLED.' 26 | ); 27 | } 28 | return; 29 | } 30 | this.enabled = value; 31 | this.beginDate = value === false ? null : new Date(); 32 | 33 | if (sendLogs) { 34 | this.log.info( 35 | value ? 'panic mode is ENABLED. SDK will turn into safe mode.' : 'panic mode is DISABLED. Everything is back to normal.' 36 | ); 37 | } 38 | } 39 | 40 | public checkPanicMode(response: DecisionApiResponseData | BucketingApiResponse): void { 41 | const answer = !!response?.panic; 42 | this.setPanicModeTo(answer, { sendLogs: answer !== this.enabled }); 43 | } 44 | 45 | public shouldRunSafeMode(functionName: string, options: { logType: 'debug' | 'error' } = { logType: 'error' }): boolean { 46 | const { logType } = options; 47 | if (this.enabled) { 48 | switch (logType) { 49 | case 'debug': 50 | this.log.debug(`Can't execute '${functionName}' because the SDK is in panic mode !`); 51 | break; 52 | 53 | default: 54 | this.log.error(`Can't execute '${functionName}' because the SDK is in panic mode !`); 55 | break; 56 | } 57 | } 58 | 59 | return this.enabled; 60 | } 61 | } 62 | 63 | export default PanicMode; 64 | -------------------------------------------------------------------------------- /src/class/panicMode/types.ts: -------------------------------------------------------------------------------- 1 | export type SetPanicModeToOptions = { 2 | sendLogs: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /src/config/default.ts: -------------------------------------------------------------------------------- 1 | import { FlagshipSdkInternalConfig, FlagshipSdkConfig } from '../types'; 2 | 3 | const defaultConfig: FlagshipSdkConfig = { 4 | fetchNow: true, 5 | activateNow: false, 6 | enableConsoleLogs: false, 7 | enableClientCache: true, // this setting is ignored on server side. 8 | decisionMode: 'API', 9 | nodeEnv: 'production', 10 | flagshipApi: 'https://decision-api.flagship.io/v1/', 11 | pollingInterval: null, // seconds 12 | apiKey: null, // TODO: remove next major release 13 | timeout: 2, // seconds 14 | initialBucketing: null, 15 | initialModifications: null, 16 | internal: { 17 | react: null, 18 | reactNative: { 19 | httpCallback: null 20 | } 21 | } 22 | }; 23 | 24 | export const internalConfig: FlagshipSdkInternalConfig = { 25 | campaignNormalEndpoint: '@API_URL@@ENV_ID@/campaigns?mode=normal', 26 | bucketingEndpoint: 'https://cdn.flagship.io/@ENV_ID@/bucketing.json', 27 | apiV1: 'https://decision-api.flagship.io/v1/', 28 | apiV2: 'https://decision.flagship.io/v2/', 29 | pollingIntervalMinValue: 1 // (= 1 sec) 30 | }; 31 | 32 | export default defaultConfig; 33 | -------------------------------------------------------------------------------- /src/config/otherSdk.ts: -------------------------------------------------------------------------------- 1 | const react = { 2 | enableErrorLayout: false, 3 | enableSafeMode: false, 4 | }; 5 | 6 | const reactNative = { 7 | 8 | }; 9 | 10 | const otherSdk = { 11 | ...react, 12 | ...reactNative, 13 | }; 14 | 15 | export default otherSdk; 16 | -------------------------------------------------------------------------------- /src/config/test.ts: -------------------------------------------------------------------------------- 1 | import defaultConfig from './default'; 2 | import { FlagshipSdkConfig } from '../types'; 3 | import demoData from '../../test/mock/demoData'; 4 | 5 | const testConfig: FlagshipSdkConfig = { 6 | ...defaultConfig, 7 | nodeEnv: 'development' 8 | }; 9 | 10 | export const bucketingMinimumConfig: FlagshipSdkConfig = { 11 | ...testConfig, 12 | fetchNow: true, 13 | decisionMode: 'Bucketing' 14 | }; 15 | 16 | export const bucketingApiMockOtherResponse200: { status: number; headers: { 'last-modified': string } } = { 17 | status: 200, 18 | headers: { 'last-modified': demoData.bucketing.headers.lastModified[0] } 19 | }; 20 | 21 | export const bucketingApiMockOtherResponse304: { status: number; headers: {} } = { 22 | status: 304, 23 | headers: {} // NOTE: 'last-modified' does not exist on 304 24 | }; 25 | 26 | export default testConfig; 27 | -------------------------------------------------------------------------------- /src/config/test_constants.ts: -------------------------------------------------------------------------------- 1 | export const demoPollingInterval = 0.005; // 1 000 * 0.022 = 1320 ms (1.3 sec) 2 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import flagship from './index'; 2 | import Flagship from './class/flagship/flagship'; 3 | import testConfig from './config/test'; 4 | import defaultConfig, { internalConfig } from './config/default'; 5 | import demoData from '../test/mock/demoData'; 6 | 7 | const randomUUID = 'e375004d-1fe3-4dc4-ba28-32b7fdf363ed'; 8 | 9 | describe('Flagship initialization', () => { 10 | let spyWarnLogs; 11 | let spyErrorLogs; 12 | let spyInfoLogs; 13 | beforeEach(() => { 14 | spyWarnLogs = jest.spyOn(console, 'warn').mockImplementation(); 15 | spyErrorLogs = jest.spyOn(console, 'error').mockImplementation(); 16 | spyInfoLogs = jest.spyOn(console, 'log').mockImplementation(); 17 | }); 18 | afterEach(() => { 19 | spyWarnLogs.mockRestore(); 20 | spyErrorLogs.mockRestore(); 21 | spyInfoLogs.mockRestore(); 22 | }); 23 | it('start should return a Flagship instance', () => { 24 | const sdk = flagship.start(randomUUID, demoData.apiKey[0], testConfig); 25 | expect(sdk).toBeInstanceOf(Flagship); 26 | }); 27 | 28 | it('start should take default config if none is set', () => { 29 | const sdk = flagship.start(randomUUID, demoData.apiKey[0]); 30 | expect(sdk.config).toMatchObject({ ...defaultConfig, apiKey: demoData.apiKey[0], flagshipApi: internalConfig.apiV2 }); 31 | }); 32 | 33 | it('start should consider custom config if exist and override default config', () => { 34 | const customConfig = { ...testConfig, nodeEnv: 'debug' }; 35 | const sdk = flagship.start(randomUUID, demoData.apiKey[0], customConfig); 36 | expect(sdk.config).toMatchObject({ 37 | ...defaultConfig, 38 | ...customConfig, 39 | apiKey: demoData.apiKey[0], 40 | flagshipApi: internalConfig.apiV2 41 | }); 42 | }); 43 | 44 | it('start should log when a setting is not recognized except for React special settings', () => { 45 | const customConfig = { 46 | ...testConfig, 47 | fetchNow: false, 48 | timeout: 'hello', 49 | nodeEnv: 'debug', 50 | enableConsoleLogs: true, 51 | unknownSettings: 'hello world' 52 | }; 53 | const sdk = flagship.start(randomUUID, demoData.apiKey[0], customConfig); 54 | const splitElement1 = spyWarnLogs.mock.calls[0][0].split(' - '); 55 | const splitElement2 = spyWarnLogs.mock.calls[1][0].split(' - '); 56 | expect(sdk.config).toEqual({ 57 | ...defaultConfig, 58 | apiKey: demoData.apiKey[0], 59 | enableConsoleLogs: true, 60 | fetchNow: false, 61 | flagshipApi: internalConfig.apiV2, 62 | nodeEnv: 'debug' 63 | }); 64 | expect(spyWarnLogs).toHaveBeenCalledTimes(2); 65 | 66 | expect(splitElement1[2]).toEqual('Unknown key "unknownSettings" detected (with value="hello world"). This key has been ignored...'); 67 | expect(splitElement2[2]).toEqual( 68 | '"timeout" setting is incorrect (value specified =>"hello"). The default value (=2 seconds) has been set instead.' 69 | ); 70 | }); 71 | 72 | it('start should warn deprecated start even if apiKey is defined in the settings', () => { 73 | const customConfig = { 74 | ...testConfig, 75 | fetchNow: false, 76 | nodeEnv: 'debug', 77 | enableConsoleLogs: true, 78 | apiKey: demoData.apiKey[0], 79 | unknownSettings: 'hello world' 80 | }; 81 | const sdk = flagship.start(randomUUID, customConfig); 82 | const splitElement1 = spyWarnLogs.mock.calls[0][0].split(' - '); 83 | const splitElement2 = spyWarnLogs.mock.calls[1][0].split(' - '); 84 | expect(sdk.config).toEqual({ 85 | ...defaultConfig, 86 | apiKey: demoData.apiKey[0], 87 | enableConsoleLogs: true, 88 | fetchNow: false, 89 | flagshipApi: 'https://decision-api.flagship.io/v1/', 90 | nodeEnv: 'debug' 91 | }); 92 | expect(spyWarnLogs).toHaveBeenCalledTimes(2); 93 | 94 | // TODO: temporary until major release 95 | expect(splitElement1[2]).toEqual( 96 | 'WARNING: "start" function signature will change in the next major release. "start(envId, settings)" will be "start(envId, apiKey, settings)", please make this change ASAP!' 97 | ); 98 | expect(splitElement2[2]).toEqual('Unknown key "unknownSettings" detected (with value="hello world"). This key has been ignored...'); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Flagship from './class/flagship/flagship'; 2 | import { FlagshipNodeSdk, FlagshipSdkConfig, IFlagship } from './types'; 3 | 4 | function startLegacy(envId: string, config?: FlagshipSdkConfig): IFlagship { 5 | return new Flagship(envId, undefined, config); 6 | } 7 | 8 | // NOTE: apiKeyOrSettings (any) will become apiKey (string) in next major release 9 | function start(envId: string, apiKeyOrSettings?: any, config?: FlagshipSdkConfig): IFlagship { 10 | if (typeof apiKeyOrSettings === 'object' && apiKeyOrSettings !== null && !Array.isArray(apiKeyOrSettings)) { 11 | return startLegacy(envId, apiKeyOrSettings as FlagshipSdkConfig); 12 | } 13 | return new Flagship(envId, apiKeyOrSettings as string, config); 14 | } 15 | 16 | const flagship: FlagshipNodeSdk = { 17 | start 18 | }; 19 | 20 | export default flagship as FlagshipNodeSdk; 21 | -------------------------------------------------------------------------------- /src/lib/axiosHelper.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const instance = axios.create(); 4 | 5 | if (typeof window === 'undefined') { 6 | const { Agent: HttpsAgent } = require('https'); 7 | const { Agent: HttpAgent } = require('http'); 8 | instance.defaults.httpAgent = new HttpAgent({ keepAlive: true }); 9 | instance.defaults.httpsAgent = new HttpsAgent({ keepAlive: true }); 10 | } 11 | export const defaultAxios = axios; 12 | export default instance; 13 | -------------------------------------------------------------------------------- /src/lib/index.test.ts: -------------------------------------------------------------------------------- 1 | import defaultConfig, { internalConfig } from '../config/default'; 2 | import { FlagshipSdkConfig } from '../types'; 3 | import flagshipSdkHelper from './flagshipSdkHelper'; 4 | 5 | describe('Flagship helpers', () => { 6 | let fakeSdkConfig: FlagshipSdkConfig; 7 | beforeEach(() => { 8 | fakeSdkConfig = defaultConfig; 9 | }); 10 | afterEach(() => { 11 | // NOHTING 12 | }); 13 | describe('flagshipSdkHelper', () => { 14 | it('isUsingFlagshipApi works good', () => { 15 | expect(flagshipSdkHelper.isUsingFlagshipApi('v1', fakeSdkConfig)).toEqual(true); 16 | expect(flagshipSdkHelper.isUsingFlagshipApi('v2', fakeSdkConfig)).toEqual(false); 17 | expect(flagshipSdkHelper.isUsingFlagshipApi('v1010', fakeSdkConfig)).toEqual(false); 18 | 19 | fakeSdkConfig = { ...fakeSdkConfig, flagshipApi: internalConfig.apiV2 }; 20 | expect(flagshipSdkHelper.isUsingFlagshipApi('v1', fakeSdkConfig)).toEqual(false); 21 | expect(flagshipSdkHelper.isUsingFlagshipApi('v2', fakeSdkConfig)).toEqual(true); 22 | expect(flagshipSdkHelper.isUsingFlagshipApi('v1010', fakeSdkConfig)).toEqual(false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/lib/loggerHelper.ts: -------------------------------------------------------------------------------- 1 | import FlagshipLogger from '@flagship.io/js-sdk-logs'; 2 | 3 | export default FlagshipLogger; 4 | -------------------------------------------------------------------------------- /src/lib/utils.test.ts: -------------------------------------------------------------------------------- 1 | import utilsHelper from './utils'; 2 | 3 | describe('Utils helper', () => { 4 | it('should work', () => { 5 | expect(utilsHelper.deepCompare({ a: 1 }, { b: 1 })).toEqual(false); 6 | expect(utilsHelper.deepCompare({ a: 1 }, { a: 1 })).toEqual(true); 7 | 8 | expect(utilsHelper.deepCompare({ a: { b: 1 } }, { a: { b: 2 } })).toEqual(false); 9 | expect(utilsHelper.deepCompare({ a: { b: 1 } }, { a: { b: 1 } })).toEqual(true); 10 | 11 | expect(utilsHelper.deepCompare({ a: { b: 1 } }, { a: 1, b: 2 })).toEqual(false); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export type DepCompareArgs = { [key: string]: any }; 2 | 3 | const utilsHelper = { 4 | deepCompare: (json1: DepCompareArgs, json2: DepCompareArgs): boolean => { 5 | if (Object.prototype.toString.call(json1) === Object.prototype.toString.call(json2)) { 6 | if (Object.prototype.toString.call(json1) === '[object Object]' || Object.prototype.toString.call(json1) === '[object Array]') { 7 | if (Object.keys(json1).length !== Object.keys(json2).length) { 8 | return false; 9 | } 10 | return Object.keys(json1).every(function (key) { 11 | return utilsHelper.deepCompare(json1[key], json2[key]); 12 | }); 13 | } 14 | return json1 === json2; 15 | } 16 | return false; 17 | }, 18 | isServer: (): boolean => !(typeof window != 'undefined' && window.document), 19 | isClient: (): boolean => !utilsHelper.isServer() 20 | }; 21 | 22 | export default utilsHelper; 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { FsLogger } from '@flagship.io/js-sdk-logs'; 3 | import { CancelTokenSource } from 'axios'; 4 | import { 5 | FlagshipVisitorContext, 6 | FsModifsRequestedList, 7 | DecisionApiResponse, 8 | HitShape, 9 | GetModificationsOutput, 10 | DecisionApiCampaign, 11 | GetModificationInfoOutput, 12 | DecisionApiResponseData, 13 | DecisionApiSimpleResponse, 14 | ModificationsInternalStatus 15 | } from './class/flagshipVisitor/types'; 16 | import { BucketingApiResponse } from './class/bucketing/types'; 17 | import { SetPanicModeToOptions } from './class/panicMode/types'; 18 | 19 | export type MurmurV3 = (value: string, seed?: number) => number; 20 | 21 | export type PostFlagshipApiCallback = ( 22 | axiosCallback: () => Promise, 23 | cancelTokenSource: CancelTokenSource, 24 | config: FlagshipSdkConfig 25 | ) => Promise; 26 | 27 | export interface IFsVisitorProfile { 28 | id: string; // required 29 | anonymousId?: string | null; // optional 30 | context?: FlagshipVisitorContext; // optional 31 | campaigns?: DecisionApiCampaign[]; // optional 32 | } 33 | 34 | export interface IFsCacheManager { 35 | saveVisitorProfile: (visitorId: string, visitorProfile: IFsVisitorProfile) => void; 36 | loadVisitorProfile: (visitorId: string) => IFsVisitorProfile | null; 37 | } 38 | 39 | export type IFlagshipCore = { 40 | log: FsLogger; 41 | config: FlagshipSdkConfig; 42 | envId: string; 43 | cacheManager: IFsCacheManager | null; // only defined on client side for now... 44 | panic: IFsPanicMode; 45 | }; 46 | 47 | export type FlagshipSdkConfig = { 48 | fetchNow?: boolean; 49 | pollingInterval?: number | null; 50 | activateNow?: boolean; 51 | enableConsoleLogs?: boolean; 52 | decisionMode?: 'API' | 'Bucketing'; 53 | nodeEnv?: string; 54 | enableClientCache?: boolean; 55 | flagshipApi?: string; 56 | apiKey?: string | null; 57 | initialModifications?: DecisionApiCampaign[] | null; 58 | initialBucketing?: BucketingApiResponse | null; 59 | timeout?: number; 60 | internal?: { 61 | react?: {}; 62 | reactNative?: { 63 | httpCallback?: PostFlagshipApiCallback; 64 | }; 65 | }; 66 | }; 67 | 68 | export type FlagshipSdkInternalConfig = { 69 | campaignNormalEndpoint: string; 70 | bucketingEndpoint: string; 71 | apiV1: string; 72 | apiV2: string; 73 | pollingIntervalMinValue: number; 74 | }; 75 | 76 | export type SaveCacheArgs = { 77 | modifications: { 78 | before: DecisionApiCampaign[] | null; 79 | after: DecisionApiCampaign[] | null; 80 | }; 81 | saveInCacheModifications(modificationsToSaveInCache: DecisionApiCampaign[] | null): void; 82 | }; 83 | 84 | export interface IFsPanicMode { 85 | enabled: boolean; 86 | beginDate: Date | null; 87 | log: FsLogger; 88 | setPanicModeTo(value: boolean, options?: SetPanicModeToOptions): void; 89 | checkPanicMode(response: DecisionApiResponseData | BucketingApiResponse): void; 90 | shouldRunSafeMode(functionName: string, options?: { logType: 'debug' | 'error' }): boolean; 91 | } 92 | 93 | export interface IFlagshipBucketingVisitor extends IFlagshipCore { 94 | data: BucketingApiResponse | null; 95 | computedData: DecisionApiResponseData | null; 96 | visitorId: string; 97 | visitorContext: FlagshipVisitorContext; 98 | global: IFlagshipBucketing; 99 | getEligibleCampaigns(): DecisionApiCampaign[]; 100 | updateCache(): boolean; 101 | updateVisitorContext(newContext: FlagshipVisitorContext): void; 102 | } 103 | 104 | export interface IFlagshipBucketing extends EventEmitter, IFlagshipCore { 105 | data: BucketingApiResponse | null; 106 | isPollingRunning: boolean; 107 | lastModifiedDate: string | null; 108 | callApi(): Promise; 109 | startPolling(): void; 110 | stopPolling(): void; 111 | on(event: 'launched', listener: ({ status: number }) => void): this; 112 | on(event: 'error', listener: (args: Error) => void): this; 113 | } 114 | 115 | export type ReadyListenerOutput = { 116 | withError: boolean; 117 | error: Error | null; 118 | }; 119 | 120 | export interface IFlagshipVisitor extends EventEmitter, IFlagshipCore { 121 | id: string; 122 | anonymousId: string | null; 123 | context: FlagshipVisitorContext; 124 | isAllModificationsFetched: boolean; 125 | isAuthenticated: boolean; 126 | bucket: IFlagshipBucketingVisitor | null; 127 | fetchedModifications: DecisionApiCampaign[] | null; 128 | modificationsInternalStatus: ModificationsInternalStatus | null; 129 | // UPDATE VISITOR 130 | updateContext(context: FlagshipVisitorContext): void; 131 | authenticate(id: string): Promise; 132 | unauthenticate(visitorId?: string | null): Promise; 133 | // VISITOR MODIFICATIONS 134 | getModifications(modificationsRequested: FsModifsRequestedList, activateAllModifications?: boolean): GetModificationsOutput; 135 | getModificationInfo(key: string): Promise; 136 | synchronizeModifications(activate?: boolean): Promise; 137 | getModificationsForCampaign(campaignId: string, activate?: boolean): Promise; 138 | getAllModifications( 139 | activate?: boolean, 140 | options?: { force?: boolean; simpleMode?: boolean } 141 | ): Promise; 142 | activateModifications( 143 | modifications: Array<{ 144 | key: string; 145 | variationId?: string; 146 | variationGroupId?: string; 147 | }> 148 | ): void; 149 | // VISITOR HITS 150 | sendHit(hitData: HitShape): Promise; 151 | sendHits(hitsArray: Array): Promise; 152 | // VISITOR LISTENER 153 | on(event: 'ready', listener: (args: ReadyListenerOutput) => void): this; 154 | on(event: 'saveCache', listener: (args: SaveCacheArgs) => void): this; 155 | } 156 | 157 | export interface IFlagship extends IFlagshipCore { 158 | eventEmitter: EventEmitter; 159 | bucket: IFlagshipBucketing | null; 160 | newVisitor(id: string | null, context: FlagshipVisitorContext, options?: { isAuthenticated?: boolean }): IFlagshipVisitor; 161 | startBucketingPolling(): { success: boolean; reason?: string }; 162 | stopBucketingPolling(): { success: boolean; reason?: string }; 163 | } 164 | 165 | export interface FlagshipNodeSdk { 166 | start(envId: string, apiKeyOrSettings?: any, config?: FlagshipSdkConfig): IFlagship; 167 | } 168 | 169 | export type NewVisitorOptions = { 170 | isAuthenticated?: boolean; 171 | }; 172 | -------------------------------------------------------------------------------- /test/helper/assertion.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from 'axios'; 2 | import { version } from '../../package.json'; 3 | import { defaultAxios } from '../../src/lib/axiosHelper'; 4 | 5 | import { FlagshipSdkConfig, IFlagshipVisitor } from '../../src/types'; 6 | 7 | const assertionHelper = { 8 | getActivateApiCommonBody: (visitorInstance: IFlagshipVisitor): { [key: string]: string } => { 9 | return { 10 | aid: visitorInstance.anonymousId, 11 | cid: visitorInstance.envId, 12 | vid: visitorInstance.id 13 | }; 14 | }, 15 | getCommonEmptyHeaders: (): { headers: {}; cancelToken: CancelToken; timeout: undefined } => { 16 | return { 17 | headers: { 18 | 'x-sdk-client': 'js', 19 | 'x-sdk-version': version 20 | }, 21 | timeout: undefined, 22 | cancelToken: defaultAxios.CancelToken.source().token 23 | }; 24 | }, 25 | getApiKeyHeader: ( 26 | apiKey: string 27 | ): { headers: { 'x-api-key': string; 'x-sdk-client': string; 'x-sdk-version': string }; cancelToken: CancelToken } => { 28 | return { 29 | headers: { 30 | 'x-api-key': apiKey, 31 | 'x-sdk-client': 'js', 32 | 'x-sdk-version': version 33 | }, 34 | cancelToken: defaultAxios.CancelToken.source().token 35 | }; 36 | }, 37 | getCampaignsCommonBody: (visitorInstance: IFlagshipVisitor): { [key: string]: any } => { 38 | return { 39 | context: visitorInstance.context, 40 | trigger_hit: visitorInstance.config.activateNow, 41 | visitor_id: visitorInstance.id, 42 | anonymous_id: visitorInstance.anonymousId 43 | }; 44 | }, 45 | getCampaignsQueryParams: (): { params: { exposeAllKeys: boolean; sendContextEvent: boolean } } => { 46 | return { 47 | params: { 48 | exposeAllKeys: true, 49 | sendContextEvent: false 50 | } 51 | }; 52 | }, 53 | getTimeout: (url: string, config: FlagshipSdkConfig): { timeout: number } => { 54 | return { timeout: url.includes('/campaigns') ? config.timeout * 1000 : undefined }; 55 | }, 56 | extractLogsThatReportedMessage: (message: string, spyConsoleLogs: any): any[] => { 57 | return spyConsoleLogs.mock.calls.filter((call) => (call[0] as string).toLowerCase().includes(message.toLowerCase())); 58 | }, 59 | containsLogThatContainingMessage: (message: string, spyTypeLog: any): string[] => { 60 | return spyTypeLog?.mock?.calls?.filter((log) => log[0].includes(message)).map((log) => log[0]) || []; 61 | } 62 | }; 63 | 64 | export default assertionHelper; 65 | -------------------------------------------------------------------------------- /test/helper/mockGenerator.ts: -------------------------------------------------------------------------------- 1 | import PanicMode from '../../src/class/panicMode/panicMode'; 2 | import { FlagshipSdkConfig, IFsPanicMode } from '../../src/types'; 3 | 4 | type MockGenerator = { 5 | createPanicModeMock: (config: FlagshipSdkConfig, isPanic?: boolean, callback?: (PM: IFsPanicMode) => IFsPanicMode) => IFsPanicMode; 6 | }; 7 | 8 | const mockGenerator: MockGenerator = { 9 | createPanicModeMock: (config, isPanic = false, callback = (PM): IFsPanicMode => PM): IFsPanicMode => { 10 | const output = new PanicMode(config); 11 | 12 | // if (isPanic) { 13 | // output.setPanicModeTo(true, { sendLogs: false }); 14 | // } 15 | 16 | return callback(output); 17 | } 18 | }; 19 | 20 | export default mockGenerator; 21 | -------------------------------------------------------------------------------- /test/helper/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from 'jest-mock-axios/dist/lib/mock-axios-types'; 2 | import mockAxios from 'jest-mock-axios'; 3 | import demoData from '../mock/demoData'; 4 | import { internalConfig } from '../../src/config/default'; 5 | import { demoPollingInterval } from '../../src/config/test_constants'; 6 | 7 | export const mockPollingRequest = ( 8 | done, 9 | getPollingLoop: () => number, 10 | loopScheduler: (HttpResponse | string)[], 11 | envId = demoData.envId[0] 12 | ): void => { 13 | try { 14 | if (loopScheduler.length < getPollingLoop()) { 15 | return; 16 | } 17 | 18 | if (typeof loopScheduler[getPollingLoop()] === 'string') { 19 | mockAxios.mockError(loopScheduler[getPollingLoop()]); 20 | } else { 21 | mockAxios.mockResponseFor( 22 | internalConfig.bucketingEndpoint.replace('@ENV_ID@', envId), 23 | loopScheduler[getPollingLoop()] as HttpResponse 24 | ); 25 | } 26 | 27 | if (getPollingLoop() < loopScheduler.length) { 28 | mockPollingRequest(done, getPollingLoop, loopScheduler); 29 | } 30 | } catch (error) { 31 | if (error.message === 'No request to respond to!') { 32 | setTimeout(() => { 33 | mockPollingRequest(done, getPollingLoop, loopScheduler); 34 | }, demoPollingInterval - 50); 35 | } else { 36 | done.fail(`mock ${error}`); 37 | } 38 | } 39 | }; 40 | 41 | export const mockPollingRequestV2 = (index = 0, loopScheduler: (HttpResponse | string)[], options = {}): void => { 42 | const defaultOptions = { 43 | envId: demoData.envId[0], 44 | onFail: (error) => { 45 | throw new Error(`mock ${error}`); 46 | } 47 | }; 48 | const computedOptions = { ...defaultOptions, ...options }; 49 | const { envId, onFail } = computedOptions; 50 | let loopIndex = index; 51 | try { 52 | if (loopScheduler.length < loopIndex) { 53 | return; 54 | } 55 | 56 | // getting the extended info about the most recent request 57 | const lastReqInfo = mockAxios.lastReqGet(); 58 | if (lastReqInfo && lastReqInfo.url.includes('bucketing.json')) { 59 | if (typeof loopScheduler[loopIndex] === 'string') { 60 | mockAxios.mockError(loopScheduler[loopIndex], lastReqInfo); 61 | } else { 62 | mockAxios.mockResponseFor( 63 | internalConfig.bucketingEndpoint.replace('@ENV_ID@', envId), 64 | loopScheduler[loopIndex] as HttpResponse 65 | ); 66 | } 67 | // if here, means no crash 68 | loopIndex += 1; 69 | } 70 | 71 | if (loopIndex < loopScheduler.length) { 72 | setTimeout(() => { 73 | mockPollingRequestV2(loopIndex, loopScheduler, computedOptions); 74 | }, demoPollingInterval * 1000 - 25); 75 | } 76 | } catch (error) { 77 | if (error.message === 'No request to respond to!') { 78 | setTimeout(() => { 79 | mockPollingRequestV2(loopIndex, loopScheduler, computedOptions); 80 | }, demoPollingInterval * 1000 - 25); 81 | } else { 82 | const newError = error; 83 | newError.message = `mockPollingRequestV2 function - ${error.message}`; 84 | onFail(newError); 85 | } 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /test/mock/bucketing/index.js: -------------------------------------------------------------------------------- 1 | import oneCampaignOneVgMultipleTgg from './samples/oneCampaignOneVgMultipleTgg'; 2 | import badTypeBetweenTargetingAndVisitorContextKey from './samples/badTypeBetweenTargetingAndVisitorContextKey'; 3 | import classical from './samples/classical'; 4 | import fs_all_users from './samples/fs_all_users'; 5 | import fs_users from './samples/fs_users'; 6 | import panic from './samples/panic'; 7 | import containsOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/containsOperator'; 8 | import endsWithOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/endsWithOperator'; 9 | import equalsOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/equalsOperator'; 10 | import greaterThanOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/greaterThanOperator'; 11 | import greaterThanOrEqualsOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/greaterThanOrEqualsOperator'; 12 | import lowerThanOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/lowerThanOperator'; 13 | import lowerThanOrEqualsOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/lowerThanOrEqualsOperator'; 14 | import notContainsOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/notContainsOperator'; 15 | import notEqualsOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/notEqualsOperator'; 16 | import startsWithOperator from './samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/startsWithOperator'; 17 | import murmurDefaultArgs from './murmur/arguments'; 18 | import murmurLowTraffic from './murmur/badTraffic'; 19 | import murmurBadTraffic from './murmur/badTraffic2'; 20 | import murmurThreeVariations from './murmur/threeVariations'; 21 | import murmurFourVariations from './murmur/fourVariations'; 22 | import murmurExtremLowTraffic from './murmur/extremLowTraffic'; 23 | import multipleCampaigns from './samples/multipleCampaigns'; 24 | import badOperator from './samples/badOperator'; 25 | import oneCampaignWithBadTraffic from './samples/badTraffic'; 26 | import oneCampaignWith100PercentAllocation from './samples/oneCampaignWith100PercentAllocation'; 27 | import isoSdk_50_50 from './samples/isoSdk/50_50'; 28 | import isoSdk_25_25_25_25 from './samples/isoSdk/25_25_25_25'; 29 | 30 | export default { 31 | functions: { 32 | murmur: { 33 | defaultArgs: murmurDefaultArgs, 34 | lowTraffic: murmurLowTraffic, 35 | extremLowTraffic: murmurExtremLowTraffic, 36 | badTraffic: murmurBadTraffic, 37 | threeVariations: murmurThreeVariations, 38 | fourVariations: murmurFourVariations, 39 | allocation: { 40 | '0': { 41 | variationGroup: 'jzyplpoe6z3qx0fep', 42 | visitorId: '8um' 43 | }, 44 | '9': { 45 | variationGroup: 'mayaa9tovat04vvc', 46 | visitorId: 'jufp' 47 | }, 48 | '17': { 49 | variationGroup: 'vsc1rf8xs3bvu0rzs8b', 50 | visitorId: '8' 51 | }, 52 | '19': { 53 | variationGroup: 'mjfhr65cz6ctku4', 54 | visitorId: 'ylizk' 55 | }, 56 | '24': { 57 | variationGroup: 't786w88snxyg6bc', 58 | visitorId: 'wlcfe' 59 | }, 60 | '25': { 61 | variationGroup: 'wiqyggevjlzquyp', 62 | visitorId: 'gof9c' 63 | }, 64 | '31': { 65 | variationGroup: 'caktv2pfer82v3', 66 | visitorId: 'qq4k19' 67 | }, 68 | '39': { 69 | variationGroup: '4knngzgojeuj851', 70 | visitorId: 'c77ee' 71 | }, 72 | '49': { 73 | variationGroup: 'ksahzg4mupmdix', 74 | visitorId: '6xzbz5' 75 | }, 76 | '59': { 77 | variationGroup: '8ggalmzqlggx1', 78 | visitorId: 'vgatasd' 79 | }, 80 | '68': { 81 | variationGroup: 'l7jaucjpddjdwdbfgg7', 82 | visitorId: '8' 83 | }, 84 | '69': { 85 | variationGroup: '9e8h0i1l3l9hfmb', 86 | visitorId: 'bmdz9' 87 | }, 88 | '79': { 89 | variationGroup: 'ceua2wiqtqsmkue', 90 | visitorId: '7q34t' 91 | }, 92 | '89': { 93 | variationGroup: '392vsut4cnt3lptc58p', 94 | visitorId: '9' 95 | }, 96 | '99': { 97 | variationGroup: 'vfrgk91ebwf', 98 | visitorId: 'ktkzyb7z2' 99 | } 100 | } 101 | } 102 | }, 103 | isoSdk_50_50, 104 | isoSdk_25_25_25_25, 105 | headers: { 106 | lastModified: ['Wed, 18 Mar 2020 23:29:16 GMT'] 107 | }, 108 | oneCampaignWithBadTraffic, 109 | oneCampaignWith100PercentAllocation, 110 | multipleCampaigns, 111 | oneCampaignOneVgMultipleTgg, 112 | badTypeBetweenTargetingAndVisitorContextKey, 113 | classical, 114 | badOperator, 115 | fs_all_users, 116 | fs_users, 117 | panic, 118 | containsOperator, 119 | endsWithOperator, 120 | equalsOperator, 121 | greaterThanOperator, 122 | greaterThanOrEqualsOperator, 123 | lowerThanOperator, 124 | lowerThanOrEqualsOperator, 125 | notContainsOperator, 126 | notEqualsOperator, 127 | startsWithOperator 128 | }; 129 | -------------------------------------------------------------------------------- /test/mock/bucketing/murmur/arguments.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 'bptggipaqi903f3haq20', 4 | modifications: { 5 | type: 'JSON', 6 | value: { 7 | testCache: null 8 | } 9 | }, 10 | allocation: 50, 11 | reference: true 12 | }, 13 | { 14 | id: 'bptggipaqi903f3haq2g', 15 | modifications: { 16 | type: 'JSON', 17 | value: { 18 | testCache: 'value' 19 | } 20 | }, 21 | allocation: 50 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /test/mock/bucketing/murmur/badTraffic.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 'bptggipaqi903f3haq20', 4 | modifications: { 5 | type: 'JSON', 6 | value: { 7 | testCache: null 8 | } 9 | }, 10 | allocation: 50, 11 | reference: true 12 | }, 13 | { 14 | id: 'bptggipaqi903f3haq2g', 15 | modifications: { 16 | type: 'JSON', 17 | value: { 18 | testCache: 'value' 19 | } 20 | }, 21 | allocation: 30 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /test/mock/bucketing/murmur/badTraffic2.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 'bptggipaqi903f3haq20', 4 | modifications: { 5 | type: 'JSON', 6 | value: { 7 | testCache: null 8 | } 9 | }, 10 | allocation: 50, 11 | reference: true 12 | }, 13 | { 14 | id: 'bptggipaqi903f3haq2g', 15 | modifications: { 16 | type: 'JSON', 17 | value: { 18 | testCache: 'value' 19 | } 20 | }, 21 | allocation: 55 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /test/mock/bucketing/murmur/extremLowTraffic.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 'bptggipaqi903f3haq20', 4 | modifications: { 5 | type: 'JSON', 6 | value: { 7 | testCache: null 8 | } 9 | }, 10 | reference: true 11 | }, 12 | { 13 | id: 'bptggipaqi903f3haq2g', 14 | modifications: { 15 | type: 'JSON', 16 | value: { 17 | testCache: 'value' 18 | } 19 | }, 20 | allocation: 14 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /test/mock/bucketing/murmur/fourVariations.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 'bptggipaqi903f3haq20', 4 | modifications: { 5 | type: 'JSON', 6 | value: { 7 | testCache: null 8 | } 9 | }, 10 | allocation: 25, 11 | reference: true 12 | }, 13 | { 14 | id: 'bptggipaqi903f3haq2g', 15 | modifications: { 16 | type: 'JSON', 17 | value: { 18 | testCache: 'value' 19 | } 20 | }, 21 | allocation: 25 22 | }, 23 | { 24 | id: 'bptggipaqi903f3haq2p', 25 | modifications: { 26 | type: 'JSON', 27 | value: { 28 | testCache: 'value' 29 | } 30 | }, 31 | allocation: 25 32 | }, 33 | { 34 | id: 'bptggipaqi903f3haq2d', 35 | modifications: { 36 | type: 'JSON', 37 | value: { 38 | testCache: 'value' 39 | } 40 | }, 41 | allocation: 25 42 | } 43 | ]; 44 | -------------------------------------------------------------------------------- /test/mock/bucketing/murmur/threeVariations.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 'bptggipaqi903f3haq20', 4 | modifications: { 5 | type: 'JSON', 6 | value: { 7 | testCache: null 8 | } 9 | }, 10 | allocation: 33, 11 | reference: true 12 | }, 13 | { 14 | id: 'bptggipaqi903f3haq2g', 15 | modifications: { 16 | type: 'JSON', 17 | value: { 18 | testCache: 'value' 19 | } 20 | }, 21 | allocation: 33 22 | }, 23 | { 24 | id: 'bptggipaqi903f3haq2p', 25 | modifications: { 26 | type: 'JSON', 27 | value: { 28 | testCache: 'value' 29 | } 30 | }, 31 | allocation: 34 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/badOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | { 15 | operator: 'I_DONT_EXIST', 16 | key: 'isVip', 17 | value: false 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | variations: [ 24 | { 25 | id: 'bptggipaqi903f3haq20', 26 | modifications: { 27 | type: 'JSON', 28 | value: { 29 | testCache: null 30 | } 31 | }, 32 | allocation: 50, 33 | reference: true 34 | }, 35 | { 36 | id: 'bptggipaqi903f3haq2g', 37 | modifications: { 38 | type: 'JSON', 39 | value: { 40 | testCache: 'value' 41 | } 42 | }, 43 | allocation: 50 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ], 50 | panic: false 51 | }; 52 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/badTraffic.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'vfrgk91ebwf', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | { 15 | operator: 'EQUALS', 16 | key: 'fs_users', 17 | value: 'ktkzyb7z2' 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | variations: [ 24 | { 25 | id: 'bptggipaqi903f3haq20', 26 | modifications: { 27 | type: 'JSON', 28 | value: { 29 | testCache: null 30 | } 31 | }, 32 | allocation: 60, 33 | reference: true 34 | }, 35 | { 36 | id: 'bptggipaqi903f3haq2g', 37 | modifications: { 38 | type: 'JSON', 39 | value: { 40 | testCache: 'value' 41 | } 42 | }, 43 | allocation: 50 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ], 50 | panic: false 51 | }; 52 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/badTypeBetweenTargetingAndVisitorContextKey.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | { 15 | // LOWER_THAN - BAD TYPE 16 | operator: 'LOWER_THAN', 17 | key: 'lowerThanBadType', 18 | value: "I'm supposed to be a number" 19 | }, 20 | { 21 | // LOWER_THAN - BAD TYPE 22 | operator: 'LOWER_THAN', 23 | key: 'lowerThanBadTypeJson', 24 | value: { toto: 123 } 25 | }, 26 | { 27 | // LOWER_THAN - BAD TYPE 28 | operator: 'LOWER_THAN', 29 | key: 'lowerThanBadTypeArray', // 98 30 | value: [1, 2, "I'm supposed to be a number"] 31 | }, 32 | { 33 | // LOWER_THAN_OR_EQUALS - BAD TYPE 34 | operator: 'LOWER_THAN_OR_EQUALS', 35 | key: 'lowerThanBadType', 36 | value: "I'm supposed to be a number" 37 | }, 38 | { 39 | // GREATER_THAN - BAD TYPE 40 | operator: 'GREATER_THAN', 41 | key: 'lowerThanBadType', 42 | value: "I'm supposed to be a number" 43 | }, 44 | { 45 | // GREATER_THAN_OR_EQUALS - BAD TYPE 46 | operator: 'GREATER_THAN_OR_EQUALS', 47 | key: 'lowerThanBadType', 48 | value: "I'm supposed to be a number" 49 | }, 50 | { 51 | // STARTS_WITH - BAD TYPE 52 | operator: 'STARTS_WITH', 53 | key: 'lowerThanBadType', 54 | value: "I'm supposed to be a number" 55 | }, 56 | { 57 | // ENDS_WITH - BAD TYPE 58 | operator: 'ENDS_WITH', 59 | key: 'lowerThanBadType', 60 | value: "I'm supposed to be a number" 61 | }, 62 | { 63 | // CONTAINS - BAD TYPE 64 | operator: 'CONTAINS', 65 | key: 'lowerThanBadType', 66 | value: "I'm supposed to be a number" 67 | }, 68 | { 69 | // NOT_CONTAINS - BAD TYPE 70 | operator: 'NOT_CONTAINS', 71 | key: 'lowerThanBadType', 72 | value: "I'm supposed to be a number" 73 | } 74 | ] 75 | } 76 | ] 77 | }, 78 | variations: [ 79 | { 80 | id: 'bptggipaqi903f3haq20', 81 | modifications: { 82 | type: 'JSON', 83 | value: { 84 | testCache: null 85 | } 86 | }, 87 | allocation: 50, 88 | reference: true 89 | }, 90 | { 91 | id: 'bptggipaqi903f3haq2g', 92 | modifications: { 93 | type: 'JSON', 94 | value: { 95 | testCache: 'value' 96 | } 97 | }, 98 | allocation: 50 99 | } 100 | ] 101 | } 102 | ] 103 | } 104 | ], 105 | panic: false 106 | }; 107 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/classical.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'l7jaucjpddjdwdbfgg7', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | { 15 | operator: 'EQUALS', 16 | key: 'fs_all_users', 17 | value: '' 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | variations: [ 24 | { 25 | id: 'bptggipaqi903f3haq20', 26 | modifications: { 27 | type: 'JSON', 28 | value: { 29 | testCache: null 30 | } 31 | }, 32 | allocation: 50, 33 | reference: true 34 | }, 35 | { 36 | id: 'bptggipaqi903f3haq2g', 37 | modifications: { 38 | type: 'JSON', 39 | value: { 40 | testCache: 'value' 41 | } 42 | }, 43 | allocation: 50 44 | } 45 | ] 46 | } 47 | ] 48 | }, 49 | { 50 | id: 'bq4sf09oet0006cfihd0', 51 | type: 'ab', 52 | variationGroups: [ 53 | { 54 | id: 'vsc1rf8xs3bvu0rzs8b', 55 | targeting: { 56 | targetingGroups: [ 57 | { 58 | targetings: [ 59 | { 60 | operator: 'EQUALS', 61 | key: 'fs_all_users', 62 | value: '' 63 | } 64 | ] 65 | } 66 | ] 67 | }, 68 | variations: [ 69 | { 70 | id: 'bq4sf09oet0006cfiheg', 71 | modifications: { 72 | type: 'JSON', 73 | value: { 74 | 'btn-color': 'red', 75 | 'btn-text': 'Buy now !', 76 | 'txt-color': '#fff' 77 | } 78 | }, 79 | allocation: 50, 80 | reference: true 81 | }, 82 | { 83 | id: 'bq4sf09oet0006cfihf0', 84 | modifications: { 85 | type: 'JSON', 86 | value: { 87 | 'btn-color': 'green', 88 | 'btn-text': 'Buy now with discount !', 89 | 'txt-color': '#A3A3A3' 90 | } 91 | }, 92 | allocation: 50 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ], 99 | panic: false 100 | }; 101 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/fs_all_users.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | USE CASE: 4 | # test any type of 'value' 5 | 6 | */ 7 | 8 | export default { 9 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 10 | campaigns: [ 11 | { 12 | id: 'bptggipaqi903f3haq0g', 13 | type: 'ab', 14 | variationGroups: [ 15 | { 16 | id: 'bptggipaqi903f3haq1g', 17 | targeting: { 18 | targetingGroups: [ 19 | { 20 | targetings: [ 21 | { 22 | operator: 'EQUALS', 23 | key: 'fs_all_users', 24 | value: '' 25 | } 26 | ] 27 | } 28 | ] 29 | }, 30 | variations: [ 31 | { 32 | id: 'bptggipaqi903f3haq20', 33 | modifications: { 34 | type: 'JSON', 35 | value: { 36 | testCache: null 37 | } 38 | }, 39 | allocation: 50, 40 | reference: true 41 | }, 42 | { 43 | id: 'bptggipaqi903f3haq2g', 44 | modifications: { 45 | type: 'JSON', 46 | value: { 47 | testCache: 'value' 48 | } 49 | }, 50 | allocation: 50 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | ], 57 | panic: false 58 | }; 59 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/fs_users.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | USE CASE: 4 | # test any type of 'value' 5 | 6 | */ 7 | 8 | export default { 9 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 10 | campaigns: [ 11 | { 12 | id: 'bptggipaqi903f3haq0g', 13 | type: 'ab', 14 | variationGroups: [ 15 | { 16 | id: 'bptggipaqi903f3haq1g', 17 | targeting: { 18 | targetingGroups: [ 19 | { 20 | targetings: [ 21 | { 22 | operator: 'EQUALS', 23 | key: 'fs_users', 24 | value: 'test-perf' 25 | } 26 | ] 27 | } 28 | ] 29 | }, 30 | variations: [ 31 | { 32 | id: 'bptggipaqi903f3haq20', 33 | modifications: { 34 | type: 'JSON', 35 | value: { 36 | testCache: null 37 | } 38 | }, 39 | allocation: 50, 40 | reference: true 41 | }, 42 | { 43 | id: 'bptggipaqi903f3haq2g', 44 | modifications: { 45 | type: 'JSON', 46 | value: { 47 | testCache: 'value' 48 | } 49 | }, 50 | allocation: 50 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | ], 57 | panic: false 58 | }; 59 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/isoSdk/25_25_25_25.js: -------------------------------------------------------------------------------- 1 | export default { 2 | campaigns: [ 3 | { 4 | id: 'bs8qvmo4nlr01fl9aaaa', 5 | type: 'ab', 6 | variationGroups: [ 7 | { 8 | id: 'bs8qvmo4nlr01fl9bbbb', 9 | targeting: { 10 | targetingGroups: [ 11 | { 12 | targetings: [ 13 | { 14 | operator: 'EQUALS', 15 | key: 'fs_all_users', 16 | value: '' 17 | } 18 | ] 19 | } 20 | ] 21 | }, 22 | variations: [ 23 | { 24 | id: 'bs8qvmo4nlr01fl9cccc', 25 | modifications: { 26 | type: 'JSON', 27 | value: { 28 | variation: null 29 | } 30 | }, 31 | reference: true 32 | }, 33 | { 34 | id: 'bs8qvmo4nlr01fl9dddd', 35 | modifications: { 36 | type: 'JSON', 37 | value: { 38 | variation: 1 39 | } 40 | }, 41 | allocation: 25 42 | }, 43 | { 44 | id: 'bs8r09g4nlr01c77eeee', 45 | modifications: { 46 | type: 'JSON', 47 | value: { 48 | variation: 2 49 | } 50 | }, 51 | allocation: 25 52 | }, 53 | { 54 | id: 'bs8r09g4nlr01cdkffff', 55 | modifications: { 56 | type: 'JSON', 57 | value: { 58 | variation: 3 59 | } 60 | }, 61 | allocation: 25 62 | }, 63 | { 64 | id: 'bs8r09hsbs4011lbgggg', 65 | modifications: { 66 | type: 'JSON', 67 | value: { 68 | variation: 4 69 | } 70 | }, 71 | allocation: 25 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | ] 78 | }; 79 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/isoSdk/50_50.js: -------------------------------------------------------------------------------- 1 | export default { 2 | campaigns: [ 3 | { 4 | id: 'bs8r119sbs4016mehhhh', 5 | type: 'ab', 6 | variationGroups: [ 7 | { 8 | id: 'bs8r119sbs4016meiiii', 9 | targeting: { 10 | targetingGroups: [ 11 | { 12 | targetings: [ 13 | { 14 | operator: 'EQUALS', 15 | key: 'fs_all_users', 16 | value: '' 17 | } 18 | ] 19 | } 20 | ] 21 | }, 22 | variations: [ 23 | { 24 | id: 'bs8r119sbs4016mejjjj', 25 | modifications: { 26 | type: 'JSON', 27 | value: { 28 | variation50: null 29 | } 30 | }, 31 | reference: true 32 | }, 33 | { 34 | id: 'bs8r119sbs4016mekkkk', 35 | modifications: { 36 | type: 'JSON', 37 | value: { 38 | variation50: 1 39 | } 40 | }, 41 | allocation: 50 42 | }, 43 | { 44 | id: 'bs8r119sbs4016mellll', 45 | modifications: { 46 | type: 'JSON', 47 | value: { 48 | variation50: 2 49 | } 50 | }, 51 | allocation: 50 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/multipleCampaigns/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | USE CASE: 4 | # full truthy 5 | # full falsy 6 | # mixed 7 | 8 | */ 9 | 10 | export default { 11 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 12 | campaigns: [ 13 | { 14 | id: 'bptggipaqi903f3haq0g', 15 | type: 'ab', 16 | variationGroups: [ 17 | { 18 | id: 'bptggipaqi903f3haq1g', 19 | targeting: { 20 | targetingGroups: [ 21 | { 22 | targetings: [ 23 | { 24 | operator: 'EQUALS', 25 | key: 'foo1', 26 | value: 'yes1' 27 | } 28 | ] 29 | }, 30 | { 31 | targetings: [ 32 | { 33 | operator: 'EQUALS', 34 | key: 'foo2', 35 | value: 'yes2' 36 | } 37 | ] 38 | }, 39 | { 40 | targetings: [ 41 | { 42 | operator: 'EQUALS', 43 | key: 'foo3', 44 | value: 'yes3' 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | variations: [ 51 | { 52 | id: 'bptggipaqi903f3haq20', 53 | modifications: { 54 | type: 'JSON', 55 | value: { 56 | testCache: null 57 | } 58 | }, 59 | allocation: 50, 60 | reference: true 61 | }, 62 | { 63 | id: 'bptggipaqi903f3haq2g', 64 | modifications: { 65 | type: 'JSON', 66 | value: { 67 | testCache: 'value' 68 | } 69 | }, 70 | allocation: 50 71 | } 72 | ] 73 | } 74 | ] 75 | }, 76 | { 77 | id: 'bq4sf09oet0006cfihd0', 78 | type: 'ab', 79 | variationGroups: [ 80 | { 81 | id: 'bq4sf09oet0006cfihe0', 82 | targeting: { 83 | targetingGroups: [ 84 | { 85 | targetings: [ 86 | { 87 | operator: 'EQUALS', 88 | key: 'isVip', 89 | value: true 90 | } 91 | ] 92 | } 93 | ] 94 | }, 95 | variations: [ 96 | { 97 | id: 'bq4sf09oet0006cfiheg', 98 | modifications: { 99 | type: 'JSON', 100 | value: { 101 | 'btn-color': 'red', 102 | 'btn-text': 'Buy now !', 103 | 'txt-color': '#fff' 104 | } 105 | }, 106 | allocation: 50, 107 | reference: true 108 | }, 109 | { 110 | id: 'bq4sf09oet0006cfihf0', 111 | modifications: { 112 | type: 'JSON', 113 | value: { 114 | 'btn-color': 'green', 115 | 'btn-text': 'Buy now with discount !', 116 | 'txt-color': '#A3A3A3' 117 | } 118 | }, 119 | allocation: 50 120 | } 121 | ] 122 | } 123 | ] 124 | } 125 | ], 126 | panic: false 127 | }; 128 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgMultipleTgg/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | USE CASE: 4 | # full truthy 5 | # full falsy 6 | # mixed 7 | 8 | */ 9 | 10 | export default { 11 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 12 | campaigns: [ 13 | { 14 | id: 'bptiiipaqi903f3haq0g', 15 | type: 'ab', 16 | variationGroups: [ 17 | { 18 | id: 'bptggipaqi903f3haq1g', 19 | targeting: { 20 | targetingGroups: [ 21 | { 22 | targetings: [ 23 | { 24 | operator: 'EQUALS', 25 | key: 'foo1', 26 | value: 'yes1' 27 | } 28 | ] 29 | }, 30 | { 31 | targetings: [ 32 | { 33 | operator: 'EQUALS', 34 | key: 'foo2', 35 | value: 'yes2' 36 | } 37 | ] 38 | }, 39 | { 40 | targetings: [ 41 | { 42 | operator: 'EQUALS', 43 | key: 'foo3', 44 | value: 'yes3' 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | variations: [ 51 | { 52 | id: 'bptggipaqi903f3haq20', 53 | modifications: { 54 | type: 'JSON', 55 | value: { 56 | testCache: null 57 | } 58 | }, 59 | allocation: 50, 60 | reference: true 61 | }, 62 | { 63 | id: 'bptggipaqi903f3haq2g', 64 | modifications: { 65 | type: 'JSON', 66 | value: { 67 | testCache: 'value' 68 | } 69 | }, 70 | allocation: 50 71 | } 72 | ] 73 | } 74 | ] 75 | } 76 | ], 77 | panic: false 78 | }; 79 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/containsOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | CONTAINS - OPERATOR 17 | 18 | */ 19 | 20 | // CONTAINS - STRING 21 | { 22 | operator: 'CONTAINS', 23 | key: 'containsString', 24 | value: 'test' 25 | }, 26 | { 27 | // CONTAINS - STRING[] 28 | operator: 'CONTAINS', 29 | key: 'containsStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // CONTAINS - NUMBER 34 | operator: 'CONTAINS', 35 | key: 'containsNumber', 36 | value: 123 37 | }, 38 | { 39 | // CONTAINS - NUMBER[] 40 | operator: 'CONTAINS', 41 | key: 'containsNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // CONTAINS - BOOL 46 | operator: 'CONTAINS', 47 | key: 'containsBool', 48 | value: false 49 | }, 50 | { 51 | // CONTAINS - BOOL[] 52 | operator: 'CONTAINS', 53 | key: 'containsBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/endsWithOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | ENDS WITH - OPERATOR 17 | 18 | */ 19 | 20 | // ENDS_WITH - STRING 21 | { 22 | operator: 'ENDS_WITH', 23 | key: 'endsWithString', 24 | value: 'test' 25 | }, 26 | { 27 | // ENDS_WITH - STRING[] 28 | operator: 'ENDS_WITH', 29 | key: 'endsWithStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // ENDS_WITH - NUMBER 34 | operator: 'ENDS_WITH', 35 | key: 'endsWithNumber', 36 | value: 123 37 | }, 38 | { 39 | // ENDS_WITH - NUMBER[] 40 | operator: 'ENDS_WITH', 41 | key: 'endsWithNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // ENDS_WITH - BOOL 46 | operator: 'ENDS_WITH', 47 | key: 'endsWithBool', 48 | value: false 49 | }, 50 | { 51 | // ENDS_WITH - BOOL[] 52 | operator: 'ENDS_WITH', 53 | key: 'endsWithBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/equalsOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqigggf3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | EQUALS - OPERATOR 17 | 18 | */ 19 | 20 | // EQUALS - STRING 21 | { 22 | operator: 'EQUALS', 23 | key: 'equalsString', 24 | value: 'test' 25 | }, 26 | { 27 | // EQUALS - STRING[] 28 | operator: 'EQUALS', 29 | key: 'equalsStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // EQUALS - NUMBER 34 | operator: 'EQUALS', 35 | key: 'equalsNumber', 36 | value: 123 37 | }, 38 | { 39 | // EQUALS - NUMBER[] 40 | operator: 'EQUALS', 41 | key: 'equalsNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // EQUALS - BOOL 46 | operator: 'EQUALS', 47 | key: 'equalsBool', 48 | value: false 49 | }, 50 | { 51 | // EQUALS - BOOL[] 52 | operator: 'EQUALS', 53 | key: 'equalsBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/greaterThanOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | GREATER THAN - OPERATOR 17 | 18 | */ 19 | 20 | // GREATER_THAN - STRING 21 | { 22 | operator: 'GREATER_THAN', 23 | key: 'greaterThanString', 24 | value: 'test' 25 | }, 26 | { 27 | // GREATER_THAN - STRING[] 28 | operator: 'GREATER_THAN', 29 | key: 'greaterThanStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // GREATER_THAN - NUMBER 34 | operator: 'GREATER_THAN', 35 | key: 'greaterThanNumber', 36 | value: 123 37 | }, 38 | { 39 | // GREATER_THAN - NUMBER[] 40 | operator: 'GREATER_THAN', 41 | key: 'greaterThanNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // GREATER_THAN - BOOL 46 | operator: 'GREATER_THAN', 47 | key: 'greaterThanBool', 48 | value: false 49 | }, 50 | { 51 | // GREATER_THAN - BOOL[] 52 | operator: 'GREATER_THAN', 53 | key: 'greaterThanBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/greaterThanOrEqualsOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | GREATER THAN OR EQUALS - OPERATOR 17 | 18 | */ 19 | 20 | // GREATER_THAN_OR_EQUALS - STRING 21 | { 22 | operator: 'GREATER_THAN_OR_EQUALS', 23 | key: 'greaterThanOrEqualsString', 24 | value: 'test' 25 | }, 26 | { 27 | // GREATER_THAN_OR_EQUALS - STRING[] 28 | operator: 'GREATER_THAN_OR_EQUALS', 29 | key: 'greaterThanOrEqualsStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // GREATER_THAN_OR_EQUALS - NUMBER 34 | operator: 'GREATER_THAN_OR_EQUALS', 35 | key: 'greaterThanOrEqualsNumber', 36 | value: 123 37 | }, 38 | { 39 | // GREATER_THAN_OR_EQUALS - NUMBER[] 40 | operator: 'GREATER_THAN_OR_EQUALS', 41 | key: 'greaterThanOrEqualsNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // GREATER_THAN_OR_EQUALS - BOOL 46 | operator: 'GREATER_THAN_OR_EQUALS', 47 | key: 'greaterThanOrEqualsBool', 48 | value: false 49 | }, 50 | { 51 | // GREATER_THAN_OR_EQUALS - BOOL[] 52 | operator: 'GREATER_THAN_OR_EQUALS', 53 | key: 'greaterThanOrEqualsBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/lowerThanOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | LOWER THAN - OPERATOR 17 | 18 | */ 19 | 20 | // LOWER_THAN - STRING 21 | { 22 | operator: 'LOWER_THAN', 23 | key: 'lowerThanString', 24 | value: 'test' 25 | }, 26 | { 27 | // LOWER_THAN - STRING[] 28 | operator: 'LOWER_THAN', 29 | key: 'lowerThanStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // LOWER_THAN - NUMBER 34 | operator: 'LOWER_THAN', 35 | key: 'lowerThanNumber', 36 | value: 123 37 | }, 38 | { 39 | // LOWER_THAN - NUMBER[] 40 | operator: 'LOWER_THAN', 41 | key: 'lowerThanNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // LOWER_THAN - BOOL 46 | operator: 'LOWER_THAN', 47 | key: 'lowerThanBool', 48 | value: false 49 | }, 50 | { 51 | // LOWER_THAN - BOOL[] 52 | operator: 'LOWER_THAN', 53 | key: 'lowerThanBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/lowerThanOrEqualsOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | LOWER THAN OR EQUALS - OPERATOR 17 | 18 | */ 19 | 20 | // LOWER_THAN_OR_EQUALS - STRING 21 | { 22 | operator: 'LOWER_THAN_OR_EQUALS', 23 | key: 'lowerThanOrEqualsString', 24 | value: 'test' 25 | }, 26 | { 27 | // LOWER_THAN_OR_EQUALS - STRING[] 28 | operator: 'LOWER_THAN_OR_EQUALS', 29 | key: 'lowerThanOrEqualsStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // LOWER_THAN_OR_EQUALS - NUMBER 34 | operator: 'LOWER_THAN_OR_EQUALS', 35 | key: 'lowerThanOrEqualsNumber', 36 | value: 123 37 | }, 38 | { 39 | // LOWER_THAN_OR_EQUALS - NUMBER[] 40 | operator: 'LOWER_THAN_OR_EQUALS', 41 | key: 'lowerThanOrEqualsNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // LOWER_THAN_OR_EQUALS - BOOL 46 | operator: 'LOWER_THAN_OR_EQUALS', 47 | key: 'lowerThanOrEqualsBool', 48 | value: false 49 | }, 50 | { 51 | // LOWER_THAN_OR_EQUALS - BOOL[] 52 | operator: 'LOWER_THAN_OR_EQUALS', 53 | key: 'lowerThanOrEqualsBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/notContainsOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | NOT CONTAINS - OPERATOR 17 | 18 | */ 19 | 20 | // NOT_CONTAINS - STRING 21 | { 22 | operator: 'NOT_CONTAINS', 23 | key: 'notContainsString', 24 | value: 'test' 25 | }, 26 | { 27 | // NOT_CONTAINS - STRING[] 28 | operator: 'NOT_CONTAINS', 29 | key: 'notContainsStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // NOT_CONTAINS - NUMBER 34 | operator: 'NOT_CONTAINS', 35 | key: 'notContainsNumber', 36 | value: 123 37 | }, 38 | { 39 | // NOT_CONTAINS - NUMBER[] 40 | operator: 'NOT_CONTAINS', 41 | key: 'notContainsNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // NOT_CONTAINS - BOOL 46 | operator: 'NOT_CONTAINS', 47 | key: 'notContainsBool', 48 | value: false 49 | }, 50 | { 51 | // NOT_CONTAINS - BOOL[] 52 | operator: 'NOT_CONTAINS', 53 | key: 'notContainsBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/notEqualsOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | NOT EQUALS - OPERATOR 17 | 18 | */ 19 | 20 | // NOT_EQUALS - STRING 21 | { 22 | operator: 'NOT_EQUALS', 23 | key: 'notEqualsString', 24 | value: 'test' 25 | }, 26 | { 27 | // NOT_EQUALS - STRING[] 28 | operator: 'NOT_EQUALS', 29 | key: 'notEqualsStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // NOT_EQUALS - NUMBER 34 | operator: 'NOT_EQUALS', 35 | key: 'notEqualsNumber', 36 | value: 123 37 | }, 38 | { 39 | // NOT_EQUALS - NUMBER[] 40 | operator: 'NOT_EQUALS', 41 | key: 'notEqualsNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // NOT_EQUALS - BOOL 46 | operator: 'NOT_EQUALS', 47 | key: 'notEqualsBool', 48 | value: false 49 | }, 50 | { 51 | // NOT_EQUALS - BOOL[] 52 | operator: 'NOT_EQUALS', 53 | key: 'notEqualsBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignOneVgOneTggWithAllPossibleTargetings/startsWithOperator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bptggipaqi903f3haq0g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bptggipaqi903f3haq1g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | /* 15 | 16 | STARTS WITH - OPERATOR 17 | 18 | */ 19 | 20 | // STARTS_WITH - STRING 21 | { 22 | operator: 'STARTS_WITH', 23 | key: 'startsWithString', 24 | value: 'test' 25 | }, 26 | { 27 | // STARTS_WITH - STRING[] 28 | operator: 'STARTS_WITH', 29 | key: 'startsWithStringArray', 30 | value: ['test1', 'test2', 'test3'] 31 | }, 32 | { 33 | // STARTS_WITH - NUMBER 34 | operator: 'STARTS_WITH', 35 | key: 'startsWithNumber', 36 | value: 123 37 | }, 38 | { 39 | // STARTS_WITH - NUMBER[] 40 | operator: 'STARTS_WITH', 41 | key: 'startsWithNumberArray', 42 | value: [1, 2, 3] 43 | }, 44 | { 45 | // STARTS_WITH - BOOL 46 | operator: 'STARTS_WITH', 47 | key: 'startsWithBool', 48 | value: false 49 | }, 50 | { 51 | // STARTS_WITH - BOOL[] 52 | operator: 'STARTS_WITH', 53 | key: 'startsWithBoolArray', 54 | value: [false, false, false] 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | variations: [ 61 | { 62 | id: 'bptggipaqi903f3haq20', 63 | modifications: { 64 | type: 'JSON', 65 | value: { 66 | testCache: null 67 | } 68 | }, 69 | allocation: 50, 70 | reference: true 71 | }, 72 | { 73 | id: 'bptggipaqi903f3haq2g', 74 | modifications: { 75 | type: 'JSON', 76 | value: { 77 | testCache: 'value' 78 | } 79 | }, 80 | allocation: 50 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ], 87 | panic: false 88 | }; 89 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/oneCampaignWith100PercentAllocation/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lastModifiedDate: 'Wed, 18 Mar 2020 23:29:16 GMT', 3 | campaigns: [ 4 | { 5 | id: 'bqtvkps9h7j02m34fj2g', 6 | type: 'ab', 7 | variationGroups: [ 8 | { 9 | id: 'bqtvkps9h7j02m34fj3g', 10 | targeting: { 11 | targetingGroups: [ 12 | { 13 | targetings: [ 14 | { 15 | operator: 'EQUALS', 16 | key: 'fs_all_users', 17 | value: '' 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | variations: [ 24 | { 25 | id: 'bqtvkps9h7j02m34fj40', 26 | modifications: { 27 | type: 'JSON', 28 | value: { 29 | Buttoncolor: null 30 | } 31 | }, 32 | reference: true 33 | }, 34 | { 35 | id: 'bqtvkps9h7j02m34fj4g', 36 | modifications: { 37 | type: 'JSON', 38 | value: { 39 | Buttoncolor: 'Blue' 40 | } 41 | }, 42 | allocation: 100 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /test/mock/bucketing/samples/panic.js: -------------------------------------------------------------------------------- 1 | export default { 2 | panic: true 3 | }; 4 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/complexJson.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'blntcamqmdvg04g371f0', 6 | variationGroupId: 'blntcamqmdvg04g371h0', 7 | variation: { 8 | id: 'blntcamqmdvg04g371hg', 9 | modifications: { 10 | type: 'JSON', 11 | value: { 12 | array: [1, 2, 3], 13 | complex: { 14 | carray: [ 15 | { 16 | cobject: 0 17 | } 18 | ] 19 | }, 20 | object: { 21 | value: 123456 22 | } 23 | } 24 | } 25 | } 26 | } 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/manyModifInManyCampaigns.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'blntcamqmdvg04g371f0', 6 | variationGroupId: 'blntcamqmdvg04g371h0', 7 | variation: { 8 | id: 'blntcamqmdvg04g371hg', 9 | modifications: { 10 | type: 'FLAG', 11 | value: { 12 | psp: 'dalenys', 13 | algorithmVersion: 'new' 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | id: 'bmjdprsjan0g01uq2crg', 20 | variationGroupId: 'bmjdprsjan0g01uq2csg', 21 | variation: { 22 | id: 'bmjdprsjan0g01uq2ctg', 23 | modifications: { 24 | type: 'JSON', 25 | value: { 26 | psp: 'artefact', 27 | algorithmVersion: 'new' 28 | } 29 | } 30 | } 31 | }, 32 | { 33 | id: 'bmjdprsjan0g01uq2ceg', 34 | variationGroupId: 'bmjdprsjan0g01uq2ceg', 35 | variation: { 36 | id: 'bmjdprsjan0g01uq1ctg', 37 | reference: true, 38 | modifications: { 39 | type: 'JSON', 40 | value: { 41 | algorithmVersion: 'yolo2', 42 | psp: 'yolo', 43 | hello: 'world' 44 | } 45 | } 46 | } 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/noModif.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [], 4 | }; 5 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/oneCampaignOneModif.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'blntcamqmdvg04g371f0', 6 | variationGroupId: 'blntcamqmdvg04g371h0', 7 | variation: { 8 | id: 'blntcamqmdvg04g371hg', 9 | modifications: { 10 | type: 'FLAG', 11 | value: { 12 | modif1: 'value1' 13 | } 14 | } 15 | } 16 | } 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/oneCampaignOneModifWithAnUpdate.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'blntcamqmdvg04g371f0', 6 | variationGroupId: 'blntcamqmdvg04g777h0', // simulating that the visitor match another scenario 7 | variation: { 8 | id: 'blntcamqmdvg04g777hg', 9 | modifications: { 10 | type: 'FLAG', 11 | value: { 12 | modif1: 'valueFromOtherVgId' 13 | } 14 | } 15 | } 16 | } 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/oneCampaignWithFurtherModifs.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'blntcamqmdvg04g371f0', 6 | variationGroupId: 'blntcamqmdvg04g371h0', 7 | variation: { 8 | id: 'blntcamqmdvg04g371hg', 9 | modifications: { 10 | type: 'FLAG', 11 | value: { 12 | modif1: 'value1', 13 | modif2: 'value2', 14 | modif3: 'value3' 15 | } 16 | } 17 | } 18 | } 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/oneModifInMoreThanOneCampaign.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'blntcamqmdvg04g371f0', 6 | variationGroupId: 'blntcamqmdvg04g371h0', 7 | variation: { 8 | id: 'blntcamqmdvg04g371hg', 9 | modifications: { 10 | type: 'FLAG', 11 | value: { 12 | psp: 'dalenys', 13 | }, 14 | }, 15 | }, 16 | }, 17 | { 18 | id: 'bmjdprsjan0g01uq2crg', 19 | variationGroupId: 'bmjdprsjan0g01uq2csg', 20 | variation: { 21 | id: 'bmjdprsjan0g01uq2ctg', 22 | modifications: { 23 | type: 'JSON', 24 | value: { 25 | algorithmVersion: 'new', 26 | }, 27 | }, 28 | }, 29 | }, 30 | { 31 | id: 'bmjdprsjan0g01uq2ceg', 32 | variationGroupId: 'bmjdprsjan0g01uq2ceg', 33 | variation: { 34 | id: 'bmjdprsjan0g01uq1ctg', 35 | modifications: { 36 | type: 'JSON', 37 | value: { 38 | algorithmVersion: 'yolo2', 39 | psp: 'yolo', 40 | hello: 'world', 41 | }, 42 | }, 43 | }, 44 | }, 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/panicMode.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [], 4 | panic: true 5 | }; 6 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/twoCampaignsWithSameId.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | campaigns: [ 4 | { 5 | id: 'bmjdprsjan0g01uq2crg', 6 | variationGroupId: 'blntcamqmdvg04g371h0', 7 | variation: { 8 | id: 'blntcamqmdvg04g371hg', 9 | modifications: { 10 | type: 'FLAG', 11 | value: { 12 | psp: 'dalenys', 13 | }, 14 | }, 15 | }, 16 | }, 17 | { 18 | id: 'bmjdprsjan0g01uq2crg', 19 | variationGroupId: 'bmjdprsjan0g01uq2csg', 20 | variation: { 21 | id: 'bmjdprsjan0g01uq2ctg', 22 | modifications: { 23 | type: 'JSON', 24 | value: { 25 | algorithmVersion: 'new', 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /test/mock/decisionApi/data/weirdAnswer.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visitorId: 'test-perf', 3 | data: { 4 | weirdData: 1234, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /test/mock/decisionApi/index.js: -------------------------------------------------------------------------------- 1 | import oneModifInMoreThanOneCampaign from './data/oneModifInMoreThanOneCampaign'; 2 | import manyModifInManyCampaigns from './data/manyModifInManyCampaigns'; 3 | import noModif from './data/noModif'; 4 | import weirdAnswer from './data/weirdAnswer'; 5 | import twoCampaignsWithSameId from './data/twoCampaignsWithSameId'; 6 | import oneCampaignWithFurtherModifs from './data/oneCampaignWithFurtherModifs'; 7 | import oneCampaignOneModif from './data/oneCampaignOneModif'; 8 | import oneCampaignOneModifWithAnUpdate from './data/oneCampaignOneModifWithAnUpdate'; 9 | import complexJson from './data/complexJson'; 10 | import panicMode from './data/panicMode'; 11 | 12 | export default { 13 | normalResponse: { 14 | complexJson, 15 | oneCampaignOneModif, 16 | oneCampaignOneModifWithAnUpdate, 17 | oneModifInMoreThanOneCampaign, 18 | oneCampaignWithFurtherModifs, 19 | manyModifInManyCampaigns, 20 | noModif, 21 | panicMode 22 | }, 23 | badResponse: { 24 | twoCampaignsWithSameId, 25 | weirdAnswer 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/mock/hit/event.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'Event', 3 | data: { 4 | category: 'User Engagement', 5 | action: 'signOff', 6 | label: 'yolo label ;)', 7 | value: 123, 8 | documentLocation: 'http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F', 9 | pageTitle: 'YoloTitle', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/mock/hit/item.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'Item', 3 | data: { 4 | transactionId: '12451342423', 5 | name: 'yoloItem', 6 | price: 999, 7 | code: 'yoloCode', 8 | category: 'yoloCategory', 9 | quantity: 1234444, 10 | documentLocation: 'http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F', 11 | pageTitle: 'YoloScreen', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/mock/hit/pageview.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'PageView', 3 | data: { 4 | documentLocation: 'http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F', 5 | pageTitle: 'YoloPage', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/mock/hit/screenview.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'ScreenView', 3 | data: { 4 | documentLocation: 'http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F', 5 | pageTitle: 'YoloScreen', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/mock/hit/transaction.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'Transaction', 3 | data: { 4 | transactionId: '12451342423', 5 | affiliation: 'yoloAffiliation', 6 | totalRevenue: 999, 7 | shippingCost: 888, 8 | shippingMethod: 'yoloShippingMethod', 9 | currency: 'yoloCurrency', 10 | taxes: 1234444, 11 | paymentMethod: 'yoloPaymentMethod', 12 | itemCount: 2, 13 | couponCode: 'YOLOCOUPON', 14 | documentLocation: 'http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F', 15 | pageTitle: 'YoloScreen', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "noUnusedLocals": true, 8 | "resolveJsonModule": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "target": "es5" 12 | }, 13 | "types": ["mocha", "jest"], 14 | "include": ["./src/**/*"], 15 | "exclude": ["node_modules", "**/*.test.ts"], 16 | "lib": ["es6", "es2017.object"] 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const nodeConfig = require('./webpack/config.node.js'); 2 | const browserConfig = require('./webpack/config.browser.js'); 3 | const reactNativeConfig = require('./webpack/config.reactNative.js'); 4 | const standaloneConfig = require('./webpack/config.standalone.js'); 5 | 6 | module.exports = [nodeConfig, browserConfig, reactNativeConfig, standaloneConfig]; 7 | -------------------------------------------------------------------------------- /webpack/config.base.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: { 7 | app: './src/index.ts' 8 | }, 9 | mode: 'development', 10 | devtool: 'source-map', 11 | output: { 12 | filename: 'index.js', 13 | path: path.resolve(__dirname, '../dist'), 14 | libraryExport: 'default' 15 | }, 16 | module: { 17 | exprContextCritical: false, 18 | rules: [ 19 | { 20 | test: /\.(ts|js)x?$/, 21 | enforce: 'pre', 22 | include: path.resolve(__dirname), // <-- This tell to eslint to look only in your project folder 23 | exclude: /node_modules/, 24 | use: [ 25 | { 26 | loader: 'eslint-loader', 27 | options: { 28 | cache: true, 29 | failOnError: true, 30 | failOnWarning: false 31 | } 32 | } 33 | ] 34 | }, 35 | { test: /\.tsx?$/, loader: 'ts-loader' }, 36 | { 37 | test: /\.m?js$/, 38 | exclude: /(node_modules|bower_components)/, 39 | use: [ 40 | { 41 | loader: 'babel-loader', 42 | options: { 43 | presets: ['@babel/preset-env'] 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | resolve: { 51 | extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /webpack/config.browser.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const baseConfig = require('./config.base.js'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = merge(baseConfig, { 6 | target: 'web', 7 | resolve: { 8 | alias: { 9 | http: false, 10 | https: false 11 | } 12 | }, 13 | output: { 14 | filename: 'index.browser.js', 15 | libraryTarget: 'umd', 16 | library: 'flagship', 17 | libraryExport: 'default' 18 | }, 19 | externals: [ 20 | nodeExternals({ 21 | importType: 'umd', 22 | allowlist: ['axios', 'validate.js', 'events', '@flagship.io/js-sdk-logs'] 23 | }) 24 | ] 25 | }); 26 | -------------------------------------------------------------------------------- /webpack/config.node.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const baseConfig = require('./config.base.js'); 4 | 5 | module.exports = merge(baseConfig, { 6 | target: 'node', 7 | output: { 8 | filename: 'index.node.js', 9 | libraryTarget: 'commonjs2' 10 | }, 11 | externals: [ 12 | nodeExternals({ 13 | allowlist: ['axios', 'validate.js'] 14 | }) 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /webpack/config.reactNative.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const baseConfig = require('./config.base.js'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = merge(baseConfig, { 6 | target: 'web', 7 | resolve: { 8 | alias: { 9 | http: false, 10 | https: false 11 | } 12 | }, 13 | output: { 14 | filename: 'index.reactNative.js', 15 | libraryTarget: 'umd' 16 | }, 17 | externals: [ 18 | nodeExternals({ 19 | allowlist: ['axios', 'validate.js'] 20 | }) 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /webpack/config.standalone.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const baseConfig = require('./config.base.js'); 3 | const path = require('path'); 4 | 5 | module.exports = merge(baseConfig, { 6 | mode: 'production', 7 | devtool: false, 8 | target: 'web', 9 | resolve: { 10 | alias: { 11 | http: false, 12 | https: false 13 | } 14 | }, 15 | output: { 16 | filename: 'index.standalone.js', 17 | path: path.resolve(__dirname, '../public'), 18 | library: 'Flagship', // TBC - CAP on Flagship 19 | libraryTarget: 'umd' 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /webpack/config.stats.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const devConfig = require('./config.dev.js'); 3 | const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); 4 | 5 | module.exports = merge(devConfig, { 6 | plugins: [ 7 | new BundleAnalyzerPlugin(), 8 | ], 9 | }); 10 | --------------------------------------------------------------------------------