├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── package.json ├── public ├── app.css ├── experiment_flow_sequence_diagram.jpg ├── item_1.png ├── item_2.png ├── item_3.png ├── item_4.png ├── item_5.png ├── item_6.png ├── item_7.png ├── item_8.png ├── item_9.png ├── logo.png ├── main.js ├── optimizely.min.js └── screenshot.png ├── src ├── client │ ├── index.js │ └── optimizely_manager.js ├── common │ ├── action_creators │ │ └── index.js │ ├── actions │ │ └── index.js │ ├── components │ │ ├── cart │ │ │ └── index.js │ │ ├── checkout │ │ │ ├── common │ │ │ │ ├── address_form.js │ │ │ │ └── credit_card_form.js │ │ │ ├── index.js │ │ │ ├── one_step_checkout │ │ │ │ └── index.js │ │ │ └── two_step_checkout │ │ │ │ ├── billing_info.js │ │ │ │ └── shipping_address.js │ │ ├── home │ │ │ ├── index.js │ │ │ ├── item_list.js │ │ │ └── item_list_item.js │ │ ├── pdp │ │ │ └── index.js │ │ └── shared │ │ │ └── header.js │ ├── containers │ │ └── app.js │ ├── reducers │ │ ├── cart.js │ │ ├── index.js │ │ ├── items.js │ │ └── optimizely_experiment_data.js │ ├── routes │ │ └── index.js │ ├── store │ │ └── configure_store.js │ └── utils │ │ ├── enums.js │ │ └── optimizely_manager.js ├── index.js ├── server │ ├── routes │ │ ├── api.js │ │ ├── index.js │ │ ├── static.js │ │ └── views.js │ └── services │ │ ├── cart.js │ │ ├── items.js │ │ └── page_template.js └── start.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | npm-debug.log 3 | node_modules 4 | 5 | coverage/ 6 | 7 | .idea/* 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.12.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016 Optimizely 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optimizely Isomorphic React Web App Demo 2 | 3 | This demo node web app uses the Optimizely Node SDK and Javascript SDK for A/B Testing. The web app is built using React/Redux and is an isomorphic web app. All React components are shared between the server and client and can be rendered both on the server-side and on the frontend. 4 | 5 | ## Optimizely Full Stack Overview 6 | 7 | Optimizely Full Stack allows developers to run experiments anywhere in their code! The Node and Javascript SDKs provides the core components to run a full stack experiment with Optimizely. It handles aspects like bucketing, which is used to designate users to a specific experiment variation, conversion tracking, and reporting via Optimizely’s [Stats Engine](https://www.optimizely.com/statistics/). 8 | 9 | * View the [Javascript Getting Started Guide](http://developers.optimizely.com/server/getting-started/index.html?language=javascript) 10 | 11 | * View the [Node Getting Started Guide](http://developers.optimizely.com/server/getting-started/index.html?language=node) 12 | 13 | * View the reference [documentation](http://developers.optimizely.com/server/reference/index.html?language=node). 14 | 15 | * Latest [Javascript SDK](https://github.com/optimizely/javascript-sdk) 16 | 17 | ## Setting up the Optimizely Experiment 18 | 19 | If you haven't yet, please sign up for an [Optimizely account](https://www.optimizely.com/) and create a Node Fullstack project. Once you have a project ID, replace it in the [enums file](./src/common/utils/enums.js). 20 | 21 | Then, go to the [Optimizely X dashboard](https://app.optimizely.com) and create the following experiments along with their variations. These are the experiments that we are running and the goals we are tracking in our sample app: 22 | 23 | 1. sorting_experiment 24 | 25 | *Variations:* 26 | - sort_by_price 27 | - sort_by_name 28 | 29 | *Events:* 30 | - add_to_cart --> tracks when user adds a product to their cart 31 | 32 | 2. checkout_flow_experiment 33 | 34 | *Variations:* 35 | - one_step_checkout 36 | - two_step_checkout 37 | 38 | 39 | *Events:* 40 | - checkout_complete --> tracks when user clicks on the complete checkout button. We also track revenue amount of the cart. 41 | 42 | Relevant files to look at are: 43 | 44 | 1. `src/server/routes/views.js` - This is where we compute application state using Optimizely experiment data and use it to hydrate our React components on the server-side. You can find the home page sorting logic here. 45 | 46 | 2. `src/common/action_creators/index.js` - This is where we track events on the client side. 47 | 48 | 3. `src/common/components/cart/index.js` - This is where we activate the checkout_flow experiment client-side and can be activated server-side too if we were to persist cart information. 49 | 50 | ## Sequence diagram 51 | ![Sequence diagram](./public/experiment_flow_sequence_diagram.jpg) 52 | 53 | ## How is Optimizely used 54 | 55 | The experimentation logic is not instrumented in the React component themselves. Instead we determine the experiment variations before we computed the application state and use the experiment variations to massage the application state to reflect the experiments we are running. That said, the experimentation logic runs in our Redux code inside our *action_creators*. Additionally we store the experiment data in our Redux store so the React components can access the experiment data and render themselves accordingly. 56 | 57 | ## Configure Node version 58 | 59 | Node v4.0.0 or higher is needed for compatibility with this demo app. Using [nvm](https://github.com/creationix/nvm) is recommended for installing and switching between versions of Node. 60 | 61 | ## Install the app 62 | 63 | ### Install NPM dependencies 64 | `npm install` 65 | 66 | ### Build the assets 67 | `webpack` 68 | 69 | This builds the main.js file into the `public` directory 70 | 71 | ## Configure experiment 72 | To run with your own project and experiments you can modify the keys inside of `src/common/utils/enums.js` 73 | 74 | ## Run the app 75 | `npm start` 76 | 77 | Visit [http://localhost:4242](http://localhost:4242) 78 | 79 | ## Getting Help! 80 | 81 | * Developer Docs: http://developers.optimizely.com/server 82 | * Questions? Shoot us an email at developers@optimizely.com 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-react-demo-app", 3 | "version": "1.0.0", 4 | "description": "Demo isomorphic react app using the Optimizely Node SDK and Javascript SDK for A/B Testing", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon src/start.js --exec babel-node --presets es2015,stage-2", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "dependencies": { 11 | "babel-preset-react": "^6.11.1", 12 | "bluebird": "^3.4.0", 13 | "hapi": "^13.4.0", 14 | "inert": "^4.0.0", 15 | "jquery": "^3.0.0", 16 | "lodash": "^4.13.1", 17 | "nodemon": "^1.10.0", 18 | "optimizely-oui": "^13.2.0", 19 | "@optimizely/optimizely-sdk": "^2.2.0", 20 | "react": "^15.2.1", 21 | "react-dom": "^15.2.1", 22 | "react-redux": "^4.4.5", 23 | "react-router": "^2.6.1", 24 | "redux": "^3.5.2", 25 | "redux-thunk": "^2.1.0", 26 | "request-promise": "^3.0.0", 27 | "uuid-v4": "^0.1.0", 28 | "vision": "^4.1.0", 29 | "yar": "^7.0.2" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.11.4", 33 | "babel-core": "^6.9.0", 34 | "babel-loader": "^6.2.4", 35 | "babel-polyfill": "^6.9.1", 36 | "babel-preset-es2015": "^6.9.0", 37 | "babel-preset-stage-2": "^6.11.0", 38 | "eslint-plugin-react": "5.2.2", 39 | "webpack": "^1.13.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Pathway Gothic One', sans-serif; 3 | } 4 | 5 | .app-header { 6 | border-bottom: #E5E5E5 1px solid; 7 | } 8 | 9 | .back-link { 10 | font-size: 12px; 11 | color: #666; 12 | text-decoration: underline; 13 | } 14 | 15 | .back-link-container:before { 16 | display: block; 17 | position: absolute; 18 | width: 4px; 19 | height: 4px; 20 | box-sizing: border-box; 21 | border-left: 5px solid transparent; 22 | border-bottom: 5px solid transparent; 23 | border-right: 5px solid #333; 24 | border-top: 5px solid transparent; 25 | left: -4px; 26 | top: 2px; 27 | } 28 | 29 | .cart-content { 30 | background: #fff; 31 | color: #666; 32 | border: 1px solid #f3f3f3; 33 | border-radius: 2px; 34 | margin-bottom: 20px; 35 | padding-bottom: 10px; 36 | } 37 | 38 | .cta-btn { 39 | background-color: #fa5400; 40 | border-color: #fa5400; 41 | color: #fff; 42 | width: 150px; 43 | letter-spacing: -1px; 44 | font-size: 15px; 45 | height: 40px; 46 | line-height: 38px; 47 | border-radius: 2px; 48 | text-align: center; 49 | font-family: sans-serif; 50 | } 51 | 52 | .cart-summary { 53 | background: #191919; 54 | letter-spacing: .025em; 55 | font-size: 14px; 56 | color: #efefef; 57 | border-radius: 2px; 58 | margin-bottom: 20px; 59 | } 60 | 61 | .cart-summary-column { 62 | text-align: left; 63 | display: table-cell; 64 | width: 300px; 65 | padding: 0 20px 40px; 66 | } 67 | 68 | .cart-heading { 69 | margin-bottom: 0; 70 | text-transform: uppercase; 71 | border-radius: 3px 3px 0 0; 72 | background-color: #eee; 73 | line-height: 1em; 74 | padding: 20px; 75 | font-size: 20px; 76 | font-weight: 400; 77 | } 78 | 79 | .checkout-header { 80 | letter-spacing: .025em; 81 | font-size: 20px; 82 | background: #333; 83 | min-height: 40px; 84 | color: #fff; 85 | line-height: 40px; 86 | border-top-left-radius: 2px; 87 | border-top-right-radius: 2px; 88 | padding: 8px 20px; 89 | } 90 | 91 | .checkout-section { 92 | padding: 20px; 93 | background: #eee; 94 | } 95 | 96 | .checkout-section-inner { 97 | padding: 20px; 98 | border: 1px solid #ddd; 99 | background-color: #fff; 100 | border-radius: 2px; 101 | margin-bottom: 20px; 102 | width: 450px; 103 | } 104 | 105 | .checkout-section-inner .grid { 106 | margin-bottom: 20px; 107 | } 108 | 109 | .section-title { 110 | font-size: 20px; 111 | } 112 | 113 | .price { 114 | font-size: 12px; 115 | color: #666; 116 | } 117 | 118 | .title { 119 | color: #222; 120 | } -------------------------------------------------------------------------------- /public/experiment_flow_sequence_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/experiment_flow_sequence_diagram.jpg -------------------------------------------------------------------------------- /public/item_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_1.png -------------------------------------------------------------------------------- /public/item_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_2.png -------------------------------------------------------------------------------- /public/item_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_3.png -------------------------------------------------------------------------------- /public/item_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_4.png -------------------------------------------------------------------------------- /public/item_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_5.png -------------------------------------------------------------------------------- /public/item_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_6.png -------------------------------------------------------------------------------- /public/item_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_7.png -------------------------------------------------------------------------------- /public/item_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_8.png -------------------------------------------------------------------------------- /public/item_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/item_9.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/isomorphic-react-demo-app/d0bb7854d5a7d9d7b5b16195b33b607deb322a78/public/logo.png -------------------------------------------------------------------------------- /public/optimizely.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.optimizelyClient=e():t.optimizelyClient=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.i=function(t){return t},e.d=function(t,e,r){Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=173)}([function(t,e){e.LOG_LEVEL={NOTSET:0,DEBUG:1,INFO:2,WARNING:3,ERROR:4},e.ERROR_MESSAGES={INVALID_ATTRIBUTES:"%s: Provided attributes are in an invalid format.",INVALID_BUCKETING_ID:"%s: Unable to generate hash for bucketing ID %s: %s",INVALID_DATAFILE:"%s: Datafile is invalid: %s",INVALID_JSON:"%s: JSON object is not valid.",INVALID_ERROR_HANDLER:'%s: Provided "errorHandler" is in an invalid format.',INVALID_EVENT_DISPATCHER:'%s: Provided "eventDispatcher" is in an invalid format.',INVALID_EVENT_KEY:"%s: Event key %s is not in datafile.",INVALID_EVENT_TAGS:"%s: Provided event tags are in an invalid format.",INVALID_EXPERIMENT_KEY:"%s: Experiment key %s is not in datafile.",INVALID_GROUP_ID:"%s: Group ID %s is not in datafile.",INVALID_LOGGER:'%s: Provided "logger" is in an invalid format.',INVALID_USER_ID:"%s: Provided user ID is in an invalid format.",JSON_SCHEMA_EXPECTED:"%s: JSON schema expected.",NO_DATAFILE_SPECIFIED:"%s: No datafile specified. Cannot start optimizely.",NO_JSON_PROVIDED:"%s: No JSON object to validate against schema."},e.LOG_MESSAGES={ACTIVATE_USER:"%s: Activating user %s in experiment %s.",DISPATCH_CONVERSION_EVENT:"%s: Dispatching conversion event to URL %s with params %s.",DISPATCH_IMPRESSION_EVENT:"%s: Dispatching impression event to URL %s with params %s.",DEPRECATED_EVENT_VALUE:"%s: Event value is deprecated in %s call.",EVENT_NOT_ASSOCIATED_WITH_EXPERIMENTS:"%s: Event %s is not associated with any running experiments.",EXPERIMENT_NOT_RUNNING:"%s: Experiment %s is not running.",FORCED_BUCKETING_FAILED:"%s: Variation key %s is not in datafile. Not activating user %s.",INVALID_OBJECT:"%s: Optimizely object is not valid. Failing %s.",INVALID_CLIENT_ENGINE:"%s: Invalid client engine passed: %s. Defaulting to node-sdk.",INVALID_VARIATION_ID:"%s: Bucketed into an invalid variation ID. Returning null.",NO_VALID_EXPERIMENTS_FOR_EVENT_TO_TRACK:"%s: There are no valid experiments for event %s to track.",NOT_ACTIVATING_USER:"%s: Not activating user %s for experiment %s.",NOT_TRACKING_USER:"%s: Not tracking user %s.",NOT_TRACKING_USER_FOR_EXPERIMENT:"%s: Not tracking user %s for experiment %s.",SHOULD_NOT_DISPATCH_ACTIVATE:'%s: Experiment %s is in "Launched" state. Not activating user.',SHOULD_NOT_DISPATCH_TRACK:'%s: Experiment %s is in "Launched" state. Not tracking user for it.',SKIPPING_JSON_VALIDATION:"%s: Skipping JSON schema validation.",TRACK_EVENT:"%s: Tracking event %s for user %s.",USER_ASSIGNED_TO_VARIATION_BUCKET:"%s: Assigned variation bucket %s to user %s.",USER_ASSIGNED_TO_EXPERIMENT_BUCKET:"%s: Assigned experiment bucket %s to user %s.",USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP:"%s: User %s is in experiment %s of group %s.",USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP:"%s: User %s is not in experiment %s of group %s.",USER_NOT_IN_EXPERIMENT:"%s: User %s is in no experiment.",USER_FORCED_IN_VARIATION:"%s: User %s is forced in variation %s.",USER_HAS_VARIATION:"%s: User %s is in variation %s of experiment %s.",USER_HAS_NO_VARIATION:"%s: User %s is in no variation of experiment %s.",USER_NOT_IN_ANY_EXPERIMENT:"%s: User %s is not in any experiment of group %s.",USER_NOT_IN_EXPERIMENT:"%s: User %s does not meet conditions to be in experiment %s.",VALID_DATAFILE:"%s: Datafile is valid."},e.JAVASCRIPT_CLIENT_ENGINE="javascript-sdk",e.NEW_OPTIMIZELY_VERSION="2",e.NODE_CLIENT_ENGINE="node-sdk",e.NODE_CLIENT_VERSION="1.2.1"},function(t,e){var r=Array.isArray;t.exports=r},function(t,e,r){(function(e){var n=r(103),i=n("object"==typeof e&&e),o=n("object"==typeof self&&self),a=n("object"==typeof this&&this),s=i||o||a||Function("return this")();t.exports=s}).call(e,r(17))},function(t,e){var r=function(){function t(t){return Object.prototype.toString.call(t).slice(8,-1).toLowerCase()}function e(t,e){for(var r=[];e>0;r[--e]=t);return r.join("")}var n=function(){return n.cache.hasOwnProperty(arguments[0])||(n.cache[arguments[0]]=n.parse(arguments[0])),n.format.call(null,n.cache[arguments[0]],arguments)};return n.object_stringify=function(t,e,r,i){var o="";if(null!=t)switch(typeof t){case"function":return"[Function"+(t.name?": "+t.name:"")+"]";case"object":if(t instanceof Error)return"["+t.toString()+"]";if(e>=r)return"[Object]";if(i&&(i=i.slice(0),i.push(t)),null!=t.length){o+="[";var a=[];for(var s in t)i&&i.indexOf(t[s])>=0?a.push("[Circular]"):a.push(n.object_stringify(t[s],e+1,r,i));o+=a.join(", ")+"]"}else{if("getMonth"in t)return"Date("+t+")";o+="{";var a=[];for(var u in t)t.hasOwnProperty(u)&&(i&&i.indexOf(t[u])>=0?a.push(u+": [Circular]"):a.push(u+": "+n.object_stringify(t[u],e+1,r,i)));o+=a.join(", ")+"}"}return o;case"string":return'"'+t+'"'}return""+t},n.format=function(i,o){var a,s,u,c,f,l,p,h=1,d=i.length,v="",m=[];for(s=0;s=0?"+"+a:a,l=c[4]?"0"==c[4]?"0":c[4].charAt(1):" ",p=c[6]-String(a).length,f=c[6]?e(l,p):"",m.push(c[5]?a+f:f+a)}return m.join("")},n.cache={},n.parse=function(t){for(var e=t,r=[],n=[],i=0;e;){if(null!==(r=/^[^\x25]+/.exec(e)))n.push(r[0]);else if(null!==(r=/^\x25{2}/.exec(e)))n.push("%");else{if(null===(r=/^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosOuxX])/.exec(e)))throw new Error("[sprintf] "+e);if(r[2]){i|=1;var o=[],a=r[2],s=[];if(null===(s=/^([a-z_][a-z_\d]*)/i.exec(a)))throw new Error("[sprintf] "+a);for(o.push(s[1]);""!==(a=a.substring(s[0].length));)if(null!==(s=/^\.([a-z_][a-z_\d]*)/i.exec(a)))o.push(s[1]);else{if(null===(s=/^\[(\d+)\]/.exec(a)))throw new Error("[sprintf] "+a);o.push(s[1])}r[2]=o}else i|=2;if(3===i)throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");n.push(r)}e=e.substring(r[0].length)}return n},n}(),n=function(t,e){var n=e.slice();return n.unshift(t),r.apply(null,n)};t.exports=r,r.sprintf=r,r.vsprintf=n},function(t,e,r){(function(t,n){var i;(function(){function o(t,e){return t.push.apply(t,e),t}function a(t,e,r,n){for(var i=t.length,o=r+(n?1:-1);n?o--:++o0&&r(u)?e>1?b(u,e-1,r,n,i):o(i,u):n||(i[i.length]=u)}return i}function I(t,e){return t&&Ke(t,e,rr)}function O(t,e){return _(e,function(e){return Lt(t[e])})}function x(t,e){return t>e}function A(t,e,r,n,i){return t===e||(null==t||null==e||!Ct(t)&&!Dt(e)?t!==t&&e!==e:j(t,e,A,r,n,i))}function j(t,e,r,n,i,o){var a=Xe(t),s=Xe(e),u=he,c=he;a||(u=Ce.call(t),u=u==pe?_e:u),s||(c=Ce.call(e),c=c==pe?_e:c);var f=u==_e&&!l(t),p=c==_e&&!l(e),h=u==c;o||(o=[]);var d=$e(o,function(e){return e[0]===t});if(d&&d[1])return d[1]==e;if(o.push([t,e]),h&&!f){var v=a?z(t,e,r,n,i,o):J(t,e,u,r,n,i,o);return o.pop(),v}if(!(i&ce)){var m=f&&Le.call(t,"__wrapped__"),g=p&&Le.call(e,"__wrapped__");if(m||g){var y=m?t.value():t,E=g?e.value():e,v=r(y,E,n,i,o);return o.pop(),v}}if(!h)return!1;var v=X(t,e,r,n,i,o);return o.pop(),v}function N(t){return"function"==typeof t?t:null==t?Xt:("object"==typeof t?L:C)(t)}function w(t){return Ue(Object(t))}function S(t){t=null==t?t:Object(t);var e=[];for(var r in t)e.push(r);return e}function R(t,e){return ti?0:i+e),r=r>i?i:r,r<0&&(r+=i),i=e>r?0:r-e>>>0,e>>>=0;for(var o=Array(i);++ne||o&&a&&u&&!s&&!c||n&&a&&u||!r&&u||!i)return 1;if(!n&&!o&&!c&&t1?r[i-1]:ne;for(o=t.length>3&&"function"==typeof o?(i--,o):ne,e=Object(e);++n-1?e[o?o[a]:a]:ne}}function H(t,e,r,n){function i(){for(var e=-1,s=arguments.length,u=-1,c=n.length,f=Array(c+s),l=this&&this!==Se&&this instanceof i?a:t;++us))return!1;for(var c=-1,f=!0,l=i&ue?[]:ne;++c0&&(r=e.apply(this,arguments)),t<=1&&(e=ne),r}}function Et(t){if("function"!=typeof t)throw new TypeError(oe);return function(){return!t.apply(this,arguments)}}function _t(t){return yt(2,t)}function bt(t,e){if("function"!=typeof t)throw new TypeError(oe);return e=Me(e===ne?t.length-1:Ze(e),0),function(){for(var r=arguments,n=-1,i=Me(r.length-e,0),o=Array(i);++n-1&&t%1==0&&t<=le}function Ct(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function Dt(t){return!!t&&"object"==typeof t}function Pt(t){return kt(t)&&t!=+t}function Ft(t){return null===t}function kt(t){return"number"==typeof t||Dt(t)&&Ce.call(t)==Ee}function Ut(t){return Ct(t)&&Ce.call(t)==be}function Mt(t){return"string"==typeof t||!Xe(t)&&Dt(t)&&Ce.call(t)==Ie}function Gt(t){return t===ne}function Kt(t){return At(t)?t.length?P(t):[]:zt(t)}function Bt(t){return"string"==typeof t?t:null==t?"":t+""}function qt(t,e){var r=m(t);return e?We(r,e):r}function $t(t,e){return null!=t&&Le.call(t,e)}function Ht(t,e,r){var n=null==t?ne:t[e];return n===ne&&(n=r),Lt(n)?n.call(t):n}function zt(t){return t?u(t,rr(t)):[]}function Jt(t){return t=Bt(t),t&&xe.test(t)?t.replace(Oe,f):t}function Xt(t){return t}function Zt(t){return L(We({},t))}function Yt(t,e,r){var n=rr(e),i=O(e,n);null!=r||Ct(e)&&(i.length||!n.length)||(r=e,e=t,t=this,i=O(e,rr(e)));var a=!(Ct(r)&&"chain"in r&&!r.chain),s=Lt(t);return Ge(i,function(r){var n=e[r];t[r]=n,s&&(t.prototype[r]=function(){var e=this.__chain__;if(a||e){var r=t(this.__wrapped__),i=r.__actions__=P(this.__actions__);return i.push({func:n,args:arguments,thisArg:t}),r.__chain__=e,r}return n.apply(t,o([this.value()],arguments))})}),t}function Wt(){return Se._===this&&(Se._=De),this}function Qt(){}function te(t){var e=++Ve;return Bt(t)+e}function ee(t){return t&&t.length?E(t,Xt,x):ne}function re(t){return t&&t.length?E(t,Xt,R):ne}var ne,ie="4.13.1",oe="Expected a function",ae=1,se=32,ue=1,ce=2,fe=1/0,le=9007199254740991,pe="[object Arguments]",he="[object Array]",de="[object Boolean]",ve="[object Date]",me="[object Error]",ge="[object Function]",ye="[object GeneratorFunction]",Ee="[object Number]",_e="[object Object]",be="[object RegExp]",Ie="[object String]",Oe=/[&<>"'`]/g,xe=RegExp(Oe.source),Ae={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},je=c("object"==typeof n&&n),Ne=c("object"==typeof self&&self),we=c("object"==typeof this&&this),Se=je||Ne||we||Function("return this")(),Re=Array.prototype,Te=Object.prototype,Le=Te.hasOwnProperty,Ve=0,Ce=Te.toString,De=Se._,Pe=Object.create,Fe=Te.propertyIsEnumerable,ke=Se.isFinite,Ue=Object.keys,Me=Math.max;h.prototype=m(p.prototype),h.prototype.constructor=h;var Ge=K(I),Ke=B(),Be=C("length"),qe=String,$e=$(Q),He=bt(function(t,e,r){return H(t,ae|se,e,r)}),ze=bt(function(t,e){return g(t,1,e)}),Je=bt(function(t,e,r){return g(t,Ye(e)||0,r)}),Xe=Array.isArray,Ze=Number,Ye=Number,We=G(function(t,e){M(e,rr(e),t)}),Qe=G(function(t,e){M(e,nr(e),t)}),tr=G(function(t,e,r,n){M(e,nr(e),t,n)}),er=bt(function(t){return t.push(ne,d),tr.apply(ne,t)}),rr=w,nr=S,ir=bt(function(t,e){return null==t?{}:V(t,T(b(e,1),qe))}),or=N;p.assignIn=Qe,p.before=yt,p.bind=He,p.chain=at,p.compact=Y,p.concat=W,p.create=qt,p.defaults=er,p.defer=ze,p.delay=Je,p.filter=lt,p.flatten=tt,p.flattenDeep=et,p.iteratee=or,p.keys=rr,p.map=ht,p.matches=Zt,p.mixin=Yt,p.negate=Et,p.once=_t,p.pick=ir,p.slice=ot,p.sortBy=gt,p.tap=st,p.thru=ut,p.toArray=Kt,p.values=zt,p.extend=Qe,Yt(p,p),p.clone=It,p.escape=Jt,p.every=ft,p.find=$e,p.forEach=pt,p.has=$t,p.head=rt,p.identity=Xt,p.indexOf=nt,p.isArguments=xt,p.isArray=Xe,p.isBoolean=Nt,p.isDate=wt,p.isEmpty=St,p.isEqual=Rt,p.isFinite=Tt,p.isFunction=Lt,p.isNaN=Pt,p.isNull=Ft,p.isNumber=kt,p.isObject=Ct,p.isRegExp=Ut,p.isString=Mt,p.isUndefined=Gt,p.last=it,p.max=ee,p.min=re,p.noConflict=Wt,p.noop=Qt,p.reduce=dt,p.result=Ht,p.size=vt,p.some=mt,p.uniqueId=te,p.each=pt,p.first=rt,Yt(p,function(){var t={};return I(p,function(e,r){Le.call(p.prototype,r)||(t[r]=e)}),t}(),{chain:!1}),p.VERSION=ie,Ge(["pop","join","replace","reverse","split","push","shift","sort","splice","unshift"],function(t){var e=(/^(?:replace|split)$/.test(t)?String.prototype:Re)[t],r=/^(?:push|sort|unshift)$/.test(t)?"tap":"thru",n=/^(?:pop|join|replace|shift)$/.test(t);p.prototype[t]=function(){var t=arguments;if(n&&!this.__chain__){var i=this.value();return e.apply(Xe(i)?i:[],t)}return this[r](function(r){return e.apply(Xe(r)?r:[],t)})}}),p.prototype.toJSON=p.prototype.valueOf=p.prototype.value=ct,(Ne||{})._=p,i=function(){return p}.call(e,r,e,t),!(i!==ne&&(t.exports=i))}).call(this)}).call(e,r(25)(t),r(17))},function(t,e,r){function n(t,e){var r=o(t,e);return i(r)?r:void 0}var i=r(95),o=r(122);t.exports=n},function(t,e){function r(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=r},function(t,e,r){function n(t){var e=c(t);if(!e&&!s(t))return o(t);var r=a(t),n=!!r,f=r||[],l=f.length;for(var p in t)!i(t,p)||n&&("length"==p||u(p,l))||e&&"constructor"==p||f.push(p);return f}var i=r(32),o=r(97),a=r(129),s=r(23),u=r(41),c=r(42);t.exports=n},function(t,e,r){"use strict";var n=r(52),i=e.ValidationError=function(t,e,r,n,i,o){n&&(this.property=n),t&&(this.message=t),r&&(r.id?this.schema=r.id:this.schema=r),e&&(this.instance=e),this.name=i,this.argument=o,this.stack=this.toString()};i.prototype.toString=function(){return this.property+" "+this.message};var o=e.ValidatorResult=function(t,e,r,n){this.instance=t,this.schema=e,this.propertyPath=n.propertyPath,this.errors=[],this.throwError=r&&r.throwError,this.disableFormat=r&&r.disableFormat===!0};o.prototype.addError=function(t){var e;if("string"==typeof t)e=new i(t,this.instance,this.schema,this.propertyPath);else{if(!t)throw new Error("Missing error detail");if(!t.message)throw new Error("Missing error message");if(!t.name)throw new Error("Missing validator type");e=new i(t.message,this.instance,this.schema,this.propertyPath,t.name,t.argument)}if(this.throwError)throw e;return this.errors.push(e),e},o.prototype.importErrors=function(t){if("string"==typeof t||t&&t.validatorType)this.addError(t);else if(t&&t.errors){var e=this.errors;t.errors.forEach(function(t){e.push(t)})}},o.prototype.toString=function(t){return this.errors.map(function(t,e){return e+": "+t.toString()+"\n"}).join("")},Object.defineProperty(o.prototype,"valid",{get:function(){return!this.errors.length}});var a=e.SchemaError=function t(e,r){this.message=e,this.schema=r,Error.call(this,e),Error.captureStackTrace(this,t)};a.prototype=Object.create(Error.prototype,{constructor:{value:a,enumerable:!1},name:{value:"SchemaError",enumerable:!1}});var s=e.SchemaContext=function(t,e,r,n,i){this.schema=t,this.options=e,this.propertyPath=r,this.base=n,this.schemas=i};s.prototype.resolve=function(t){return n.resolve(this.base,t)},s.prototype.makeChild=function(t,e){var r=void 0===e?this.propertyPath:this.propertyPath+c(e),i=n.resolve(this.base,t.id||""),o=new s(t,this.options,r,i,Object.create(this.schemas));return t.id&&!o.schemas[i]&&(o.schemas[i]=t),o};var u=e.FORMAT_REGEXPS={"date-time":/^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])[tT ](2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])(\.\d+)?([zZ]|[+-]([0-5][0-9]):(60|[0-5][0-9]))$/,date:/^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-(3[01]|0[1-9]|[12][0-9])$/,time:/^(2[0-4]|[01][0-9]):([0-5][0-9]):(60|[0-5][0-9])$/,email:/^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/,"ip-address":/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,ipv6:/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/,uri:/^[a-zA-Z][a-zA-Z0-9+-.]*:[^\s]*$/,color:/^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/,hostname:/^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/,"host-name":/^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/,alpha:/^[a-zA-Z]+$/,alphanumeric:/^[a-zA-Z0-9]+$/,"utc-millisec":function(t){return"string"==typeof t&&parseFloat(t)===parseInt(t,10)&&!isNaN(t)},regex:function(t){var e=!0;try{new RegExp(t)}catch(t){e=!1}return e},style:/\s*(.+?):\s*([^;]+);?/g,phone:/^\+(?:[0-9] ?){6,14}[0-9]$/};u.regexp=u.regex,u.pattern=u.regex,u.ipv4=u["ip-address"],e.isFormat=function(t,e,r){if("string"==typeof t&&void 0!==u[e]){if(u[e]instanceof RegExp)return u[e].test(t);if("function"==typeof u[e])return u[e](t)}else if(r&&r.customFormats&&"function"==typeof r.customFormats[e])return r.customFormats[e](t);return!0};var c=e.makeSuffix=function(t){return t=t.toString(),t.match(/[.\s\[\]]/)||t.match(/^[\d]/)?t.match(/^\d+$/)?"["+t+"]":"["+JSON.stringify(t)+"]":"."+t};e.deepCompareStrict=function t(e,r){if(typeof e!=typeof r)return!1;if(e instanceof Array)return r instanceof Array&&(e.length===r.length&&e.every(function(n,i){return t(e[i],r[i])}));if("object"==typeof e){if(!e||!r)return e===r;var n=Object.keys(e),i=Object.keys(r);return n.length===i.length&&n.every(function(n){return t(e[n],r[n])})}return e===r},t.exports.deepMerge=function t(e,r){var n=Array.isArray(r),i=n&&[]||{};return n?(e=e||[],i=i.concat(e),r.forEach(function(r,n){"object"==typeof r?i[n]=t(e[n],r):e.indexOf(r)===-1&&i.push(r)})):(e&&"object"==typeof e&&Object.keys(e).forEach(function(t){i[t]=e[t]}),Object.keys(r).forEach(function(n){"object"==typeof r[n]&&r[n]&&e[n]?i[n]=t(e[n],r[n]):i[n]=r[n]})),i},e.objectGetPath=function(t,e){for(var r,n=e.split("/").slice(1);"string"==typeof(r=n.shift());){var i=decodeURIComponent(r.replace(/~0/,"~").replace(/~1/g,"/"));if(!(i in t))return;t=t[i]}return t},e.encodePath=function(t){return t.map(function(t){return"/"+encodeURIComponent(t).replace(/~/g,"%7E")}).join("")}},function(t,e){function r(t){return!!t&&"object"==typeof t}t.exports=r},function(t,e,r){function n(t){var e=-1,r=t?t.length:0;for(this.clear();++e-1&&t%1==0&&t<=n}var n=9007199254740991;t.exports=r},function(t,e){var r;r=function(){return this}();try{r=r||Function("return this")()||(0,eval)("this")}catch(t){"object"==typeof window&&(r=window)}t.exports=r},function(t,e,r){function n(t){var e=-1,r=t?t.length:0;for(this.clear();++e0?r.experimentIds:null;throw new Error(s(p.INVALID_EVENT_KEY,f,e))},getTrafficAllocation:function(t,e){var r=t.experimentKeyMap[e];if(n.isEmpty(r))throw new Error(s(p.INVALID_EXPERIMENT_KEY,f,e));return r.trafficAllocation}}},function(t,e,r){var n=r(5),i=r(2),o=n(i,"Map");t.exports=o},function(t,e,r){var n=r(2),i=n.Uint8Array;t.exports=i},function(t,e){function r(t,e,r,n){var i=-1,o=t?t.length:0;for(n&&o&&(r=t[++i]);++il))return!1;var h=c.get(t);if(h)return h==e;var d=-1,v=!0,m=u&a?new i:void 0;for(c.set(t,e);++d-1&&t%1==0&&t",'"',"`"," ","\r","\n","\t"],d=["{","}","|","\\","^","`"].concat(h),v=["'"].concat(d),m=["%","/","?",";","#"].concat(v),g=["/","?","#"],y=255,E=/^[+a-z0-9A-Z_-]{0,63}$/,_=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,b={javascript:!0,"javascript:":!0},I={javascript:!0,"javascript:":!0},O={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},x=r(170);n.prototype.parse=function(t,e,r){if(!c.isString(t))throw new TypeError("Parameter 'url' must be a string, not "+typeof t);var n=t.indexOf("?"),i=n!==-1&&n127?"x":C[P];if(!D.match(E)){var k=L.slice(0,N),U=L.slice(N+1),M=C.match(_);M&&(k.push(M[1]),U.unshift(M[2])),U.length&&(s="/"+U.join(".")+s),this.hostname=k.join(".");break}}}this.hostname.length>y?this.hostname="":this.hostname=this.hostname.toLowerCase(),T||(this.hostname=u.toASCII(this.hostname));var G=this.port?":"+this.port:"",K=this.hostname||"";this.host=K+G,this.href+=this.host,T&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==s[0]&&(s="/"+s))}if(!b[d])for(var N=0,V=v.length;N0)&&r.host.split("@");A&&(r.auth=A.shift(),r.host=r.hostname=A.shift())}return r.search=t.search,r.query=t.query,c.isNull(r.pathname)&&c.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.href=r.format(),r}if(!b.length)return r.pathname=null,r.search?r.path="/"+r.search:r.path=null,r.href=r.format(),r;for(var j=b.slice(-1)[0],N=(r.host||t.host||b.length>1)&&("."===j||".."===j)||""===j,w=0,S=b.length;S>=0;S--)j=b[S],"."===j?b.splice(S,1):".."===j?(b.splice(S,1),w++):w&&(b.splice(S,1),w--);if(!E&&!_)for(;w--;w)b.unshift("..");!E||""===b[0]||b[0]&&"/"===b[0].charAt(0)||b.unshift(""),N&&"/"!==b.join("/").substr(-1)&&b.push("");var R=""===b[0]||b[0]&&"/"===b[0].charAt(0);if(x){r.hostname=r.host=R?"":b.length?b.shift():"";var A=!!(r.host&&r.host.indexOf("@")>0)&&r.host.split("@");A&&(r.auth=A.shift(),r.host=r.hostname=A.shift())}return E=E||r.host&&b.length,E&&!R&&b.unshift(""),b.length?r.pathname=b.join("/"):(r.pathname=null,r.path=null),c.isNull(r.pathname)&&c.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.auth=t.auth||r.auth,r.slashes=r.slashes||t.slashes,r.href=r.format(),r},n.prototype.parseHost=function(){var t=this.host,e=l.exec(t);e&&(e=e[0],":"!==e&&(this.port=e.substr(1)),t=t.substr(0,t.length-e.length)),t&&(this.hostname=t)}},function(t,e,r){var n=r(4),i=r(58).Promise,o="POST",a="GET";t.exports={dispatchEvent:function(t){var e=t.url,r=t.params;return new i(t.httpVerb===o?function(t,n){var i=new XMLHttpRequest;i.open(o,e,!0),i.setRequestHeader("Content-Type","application/json"),i.addEventListener("load",function(e){var r=e.target.responseText;t(r)}),i.send(JSON.stringify(r))}:function(t,n){e+="?wxhr=true",r&&(e+="&"+s(r));var i=new XMLHttpRequest;i.open(a,e,!0),i.addEventListener("load",function(e){var r=JSON.parse(e.target.responseText);t(r)}),i.send()})}};var s=function(t){return n.map(t,function(t,e){return encodeURIComponent(e)+"="+encodeURIComponent(t)}).join("&")}},function(t,e,r){function n(t){var e=t.clientEngine;if(e!==u.NODE_CLIENT_ENGINE&&e!==u.JAVASCRIPT_CLIENT_ENGINE&&(t.logger.log(g.INFO,d(y.INVALID_CLIENT_ENGINE,E,e)),e=u.NODE_CLIENT_ENGINE),this.clientEngine=e,this.clientVersion=t.clientVersion||u.NODE_CLIENT_VERSION,this.errorHandler=t.errorHandler,this.eventDispatcher=t.eventDispatcher,this.isValidInstance=t.isValidInstance,this.logger=t.logger,t.datafile)if(t.skipJSONValidation===!0)this.configObj=p.createProjectConfig(t.datafile),this.logger.log(g.INFO,d(y.SKIPPING_JSON_VALIDATION,E));else try{l.validate(h,t.datafile)&&(this.configObj=p.createProjectConfig(t.datafile),this.logger.log(g.INFO,d(y.VALID_DATAFILE,E)))}catch(t){this.isValidInstance=!1,this.logger.log(g.ERROR,t.message),this.errorHandler.handleError(t)}else this.logger.log(g.ERROR,d(m.NO_DATAFILE_SPECIFIED,E)),this.errorHandler.handleError(new Error(d(m.NO_DATAFILE_SPECIFIED,E))),this.isValidInstance=!1}var i=r(4),o=r(68),a=r(63),s=r(64),u=r(0),c=r(66),f=r(69),l=r(70),p=r(26),h=r(67),d=r(3),v=r(71),m=u.ERROR_MESSAGES,g=u.LOG_LEVEL,y=u.LOG_MESSAGES,E="OPTIMIZELY";n.prototype.activate=function(t,e,r){if(!this.isValidInstance)return this.logger.log(g.ERROR,d(y.INVALID_OBJECT,E,"activate")),null;try{if(!this.__validateInputs(e,r)||!this.__checkIfExperimentIsActive(t,e))return this.__notActivatingExperiment(t,e);var n=this.__returnForcedVariationIdIfProvided(t,e);if(!n){if(!this.__checkIfUserIsInAudience(t,e,r))return this.__notActivatingExperiment(t,e);var i=this.__buildBucketerParams(t,e);n=s.bucket(i)}var o=p.getVariationKeyFromId(this.configObj,t,n);if(!p.isRunning(this.configObj,t)){var a=d(y.SHOULD_NOT_DISPATCH_ACTIVATE,E,t);return this.logger.log(g.DEBUG,a),o}if(null===n){var u=d(y.NOT_ACTIVATING_USER,E,e,t);return this.logger.log(g.INFO,u),null}var f={attributes:r,clientEngine:this.clientEngine,clientVersion:this.clientVersion,configObj:this.configObj,experimentKey:t,userId:e,variationId:n},l=c.getImpressionEvent(f),h=d(y.DISPATCH_IMPRESSION_EVENT,E,l.url,JSON.stringify(l.params));return this.logger.log(g.DEBUG,h),this.eventDispatcher.dispatchEvent(l).then(function(){var r=d(y.ACTIVATE_USER,E,e,t);this.logger.log(g.INFO,r)}.bind(this)),o}catch(r){this.logger.log(g.ERROR,r.message);var u=d(y.NOT_ACTIVATING_USER,E,e,t);return this.logger.log(g.INFO,u),this.errorHandler.handleError(r),null}},n.prototype.track=function(t,e,r,n){if(!this.isValidInstance)return void this.logger.log(g.ERROR,d(y.INVALID_OBJECT,E,"track"));"number"==typeof n&&isFinite(n)&&(this.logger.log(g.WARNING,d(y.DEPRECATED_EVENT_VALUE,E,"track")),n={revenue:n});try{if(!this.__validateInputs(e,r,n))return;var o=p.getExperimentIdsForEvent(this.configObj,t);if(!o)return void this.logger.log(g.WARNING,d(y.EVENT_NOT_ASSOCIATED_WITH_EXPERIMENTS,E,t));var a=this.__getValidExperimentInformationForEvent(t,e,r),s=a.validExperimentKeysForEvent;if(!s.length){var u=d(y.NO_VALID_EXPERIMENTS_FOR_EVENT_TO_TRACK,E,t);return void this.logger.log(g.INFO,u)}var f=this.__getBucketedVariationIdsForUser(a,e),l=i.every(f,i.isNull);if(l){var h=d(y.NOT_TRACKING_USER,E,e);return void this.logger.log(g.INFO,h)}var v={attributes:r,clientEngine:this.clientEngine,clientVersion:this.clientVersion,configObj:this.configObj,eventKey:t,eventTags:n,userId:e,validExperimentKeysForEvent:s,variationIds:f},m=c.getConversionEvent(v),_=d(y.DISPATCH_CONVERSION_EVENT,E,m.url,JSON.stringify(m.params));this.logger.log(g.DEBUG,_),this.eventDispatcher.dispatchEvent(m).then(function(){var r=d(y.TRACK_EVENT,E,t,e);this.logger.log(g.INFO,r)}.bind(this))}catch(t){this.logger.log(g.ERROR,t.message);var h=d(y.NOT_TRACKING_USER,E,e);this.logger.log(g.INFO,h),this.errorHandler.handleError(t)}},n.prototype.getVariation=function(t,e,r){if(!this.isValidInstance)return this.logger.log(g.ERROR,d(y.INVALID_OBJECT,E,"getVariation")),null;try{if(!this.__validateInputs(e,r)||!this.__checkIfExperimentIsActive(t,e))return null;var n=this.__returnForcedVariationIdIfProvided(t,e);if(!n){if(!this.__checkIfUserIsInAudience(t,e,r))return null;var i=this.__buildBucketerParams(t,e);n=s.bucket(i)}return p.getVariationKeyFromId(this.configObj,t,n)}catch(t){return this.logger.log(g.ERROR,t.message),this.errorHandler.handleError(t),null}},n.prototype.__getValidExperimentInformationForEvent=function(t,e,r){var n=[],o={};if(this.configObj.eventKeyMap[t]){var a;i.forEach(this.configObj.eventKeyMap[t].experimentIds,function(t){if(a=this.configObj.experimentIdMap[t].key,!this.__checkIfExperimentIsActive(a,e)){var i=d(y.NOT_TRACKING_USER_FOR_EXPERIMENT,E,e,a);return void this.logger.log(g.INFO,i)}var s=this.__returnForcedVariationIdIfProvided(a,e);if(s)n.push(a),o[a]=s;else if(this.__checkIfUserIsInAudience(a,e,r))if(p.isRunning(this.configObj,a))n.push(a);else{var u=d(y.SHOULD_NOT_DISPATCH_TRACK,E,a);this.logger.log(g.DEBUG,u)}else{var i=d(y.NOT_TRACKING_USER_FOR_EXPERIMENT,E,e,a);this.logger.log(g.INFO,i)}}.bind(this))}return{validExperimentKeysForEvent:n,experimentKeyToForcedVariationIdMap:o}},n.prototype.__getBucketedVariationIdsForUser=function(t,e){try{var r=t.validExperimentKeysForEvent,n=t.experimentKeyToForcedVariationIdMap,o=i.map(r,function(t){var r=n[t];if(r)return r;var i=this.__buildBucketerParams(t,e);return s.bucket(i)}.bind(this));return o}catch(t){return this.logger.log(g.ERROR,t.message),this.errorHandler.handleError(t),null}},n.prototype.__validateInputs=function(t,e,r){try{return v.validate(t),e&&o.validate(e),r&&f.validate(r),!0}catch(t){return this.logger.log(g.ERROR,t.message),this.errorHandler.handleError(t),!1}},n.prototype.__checkIfExperimentIsActive=function(t,e){if(!p.isActive(this.configObj,t)){var r=d(y.EXPERIMENT_NOT_RUNNING,E,t);return this.logger.log(g.INFO,r),!1}return!0},n.prototype.__returnForcedVariationIdIfProvided=function(t,e){var r=this.configObj.experimentKeyMap[t];if(i.isEmpty(r))throw new Error(d(m.INVALID_EXPERIMENT_KEY,E,t));return!i.isEmpty(r.forcedVariations)&&r.forcedVariations.hasOwnProperty(e)?s.forcedBucket(e,r.forcedVariations,t,this.configObj.experimentVariationKeyMap,this.logger):null},n.prototype.__checkIfUserIsInAudience=function(t,e,r){var n=p.getAudiencesForExperiment(this.configObj,t);if(!a.evaluate(n,r)){var i=d(y.USER_NOT_IN_EXPERIMENT,E,e,t);return this.logger.log(g.INFO,i),!1}return!0},n.prototype.__notActivatingExperiment=function(t,e){var r=d(y.NOT_ACTIVATING_USER,E,e,t);return this.logger.log(g.INFO,r),null},n.prototype.__buildBucketerParams=function(t,e){var r={};return r.experimentKey=t,r.experimentId=p.getExperimentId(this.configObj,t),r.userId=e,r.trafficAllocationConfig=p.getTrafficAllocation(this.configObj,t),r.experimentKeyMap=this.configObj.experimentKeyMap,r.groupIdMap=this.configObj.groupIdMap,r.experimentVariationKeyMap=this.configObj.experimentVariationKeyMap,r.variationIdMap=this.configObj.variationIdMap,r.logger=this.logger,r},t.exports=n},function(t,e){t.exports={handleError:function(t){}}},function(t,e,r){function n(){}function i(t){t=s.assignIn({logLevel:u.LOG_LEVEL.ERROR,logToConsole:!0,prefix:"[OPTIMIZELY]"},t),this.setLogLevel(t.logLevel),this.logToConsole=t.logToConsole,this.prefix=t.prefix}function o(t){switch(t){case u.LOG_LEVEL.DEBUG:return"DEBUG";case u.LOG_LEVEL.INFO:return"INFO";case u.LOG_LEVEL.WARNING:return"WARNING";case u.LOG_LEVEL.ERROR:return"ERROR";default:return"NOTSET"}}function a(){return new Date}var s=r(4),u=r(0);n.prototype.log=function(){},i.prototype.log=function(t,e){this.__shouldLog(t)&&(this.prefix&&(e=this.prefix+" - "+this.logLevelName+" "+a()+" "+e),this.logToConsole&&this.__consoleLog(t,[e]))},i.prototype.setLogLevel=function(t){this.logLevel=s.values(u.LOG_LEVEL).indexOf(t)>-1?t:u.LOG_LEVEL.ERROR,this.logLevelName=o(this.logLevel),this.log("Setting log level to "+t)},i.prototype.__shouldLog=function(t){return t>=this.logLevel},i.prototype.__consoleLog=function(t,e){switch(t){case u.LOG_LEVEL.DEBUG:console.log.apply(console,e);break;case u.LOG_LEVEL.INFO:console.log.apply(console,e);break;case u.LOG_LEVEL.WARNING:console.warn.apply(console,e);break;case u.LOG_LEVEL.ERROR:console.error.apply(console,e);break;default:console.log.apply(console,e)}},t.exports={createLogger:function(t){return new i(t)},createNoOpLogger:function(){return new n}}},function(t,e,r){var n=r(3),i=r(0).ERROR_MESSAGES,o="CONFIG_VALIDATOR";t.exports={validate:function(t){if(t.errorHandler&&"function"!=typeof t.errorHandler.handleError)throw new Error(n(i.INVALID_ERROR_HANDLER,o));if(t.eventDispatcher&&"function"!=typeof t.eventDispatcher.dispatchEvent)throw new Error(n(i.INVALID_EVENT_DISPATCHER,o));if(t.logger&&"function"!=typeof t.logger.log)throw new Error(n(i.INVALID_LOGGER,o));return!0}}},function(t,e,r){(function(e,n){!function(e,r){t.exports=r()}(this,function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function i(t){return"function"==typeof t}function o(t){X=t}function a(t){Z=t}function s(){return function(){return e.nextTick(p)}}function u(){return function(){J(p)}}function c(){var t=0,e=new Q(p),r=document.createTextNode("");return e.observe(r,{characterData:!0}),function(){r.data=t=++t%2}}function f(){var t=new MessageChannel;return t.port1.onmessage=p,function(){return t.port2.postMessage(0)}}function l(){var t=setTimeout;return function(){return t(p,1)}}function p(){for(var t=0;t"||t+""});i.addError({name:"type",argument:a,message:"is not of a type(s) "+a})}return i},f.anyOf=function(t,e,r,i){if(void 0===t)return null;var o=new s(t,e,r,i);if(!(e.anyOf instanceof Array))throw new u("anyOf must be an array");if(!e.anyOf.some(n.bind(this,t,r,i))){var a=e.anyOf.map(function(t,e){return t.id&&"<"+t.id+">"||t.title&&JSON.stringify(t.title)||t.$ref&&"<"+t.$ref+">"||"[subschema "+e+"]"});o.addError({name:"anyOf",argument:a,message:"is not any of "+a.join(",")})}return o},f.allOf=function(t,e,r,n){if(void 0===t)return null;if(!(e.allOf instanceof Array))throw new u("allOf must be an array");var i=new s(t,e,r,n),o=this;return e.allOf.forEach(function(e,a){var s=o.validateSchema(t,e,r,n);if(!s.valid){var u=e.id&&"<"+e.id+">"||e.title&&JSON.stringify(e.title)||e.$ref&&"<"+e.$ref+">"||"[subschema "+a+"]";i.addError({name:"allOf",argument:{id:u,length:s.errors.length,valid:s},message:"does not match allOf schema "+u+" with "+s.errors.length+" error[s]:"}),i.importErrors(s)}}),i},f.oneOf=function(t,e,r,i){if(void 0===t)return null;if(!(e.oneOf instanceof Array))throw new u("oneOf must be an array");var o=new s(t,e,r,i),a=e.oneOf.filter(n.bind(this,t,r,i)).length,c=e.oneOf.map(function(t,e){return t.id&&"<"+t.id+">"||t.title&&JSON.stringify(t.title)||t.$ref&&"<"+t.$ref+">"||"[subschema "+e+"]"});return 1!==a&&o.addError({name:"oneOf",argument:c,message:"is not exactly one from "+c.join(",")}),o},f.properties=function(t,e,r,n){if(void 0!==t&&t instanceof Object){var i=new s(t,e,r,n),o=e.properties||{};for(var a in o){var u=(t||void 0)&&t[a],c=this.validateSchema(u,o[a],r,n.makeChild(o[a],a));c.instance!==i.instance[a]&&(i.instance[a]=c.instance),i.importErrors(c)}return i}},f.patternProperties=function(t,e,r,n){if(void 0!==t&&this.types.object(t)){var o=new s(t,e,r,n),a=e.patternProperties||{};for(var u in t){var c=!0;for(var f in a){var l=new RegExp(f);if(l.test(u)){c=!1;var p=this.validateSchema(t[u],a[f],r,n.makeChild(a[f],u));p.instance!==o.instance[u]&&(o.instance[u]=p.instance),o.importErrors(p)}}c&&i.call(this,t,e,r,n,u,o)}return o}},f.additionalProperties=function(t,e,r,n){if(void 0!==t&&this.types.object(t)){if(e.patternProperties)return null;var o=new s(t,e,r,n);for(var a in t)i.call(this,t,e,r,n,a,o);return o}},f.minProperties=function(t,e,r,n){if(!t||"object"!=typeof t)return null;var i=new s(t,e,r,n),o=Object.keys(t);return o.length>=e.minProperties||i.addError({name:"minProperties",argument:e.minProperties,message:"does not meet minimum property length of "+e.minProperties}),i},f.maxProperties=function(t,e,r,n){if(!t||"object"!=typeof t)return null;var i=new s(t,e,r,n),o=Object.keys(t);return o.length<=e.maxProperties||i.addError({name:"maxProperties",argument:e.maxProperties,message:"does not meet maximum property length of "+e.maxProperties}),i},f.items=function(t,e,r,n){if(!(t instanceof Array))return null;var i=this,o=new s(t,e,r,n);return void 0!==t&&e.items?(t.every(function(t,a){var s=e.items instanceof Array?e.items[a]||e.additionalItems:e.items;if(void 0===s)return!0;if(s===!1)return o.addError({name:"items",message:"additionalItems not permitted"}),!1;var u=i.validateSchema(t,s,r,n.makeChild(s,a));return u.instance!==o.instance[a]&&(o.instance[a]=u.instance),o.importErrors(u),!0}),o):o},f.minimum=function(t,e,r,n){if("number"!=typeof t)return null;var i=new s(t,e,r,n),o=!0;return o=e.exclusiveMinimum&&e.exclusiveMinimum===!0?t>e.minimum:t>=e.minimum,o||i.addError({name:"minimum",argument:e.minimum,message:"must have a minimum value of "+e.minimum}),i},f.maximum=function(t,e,r,n){if("number"!=typeof t)return null;var i,o=new s(t,e,r,n);return i=e.exclusiveMaximum&&e.exclusiveMaximum===!0?t=e.minLength||i.addError({name:"minLength",argument:e.minLength,message:"does not meet minimum length of "+e.minLength}),i},f.maxLength=function(t,e,r,n){if("string"!=typeof t)return null;var i=new s(t,e,r,n);return t.length<=e.maxLength||i.addError({name:"maxLength",argument:e.maxLength,message:"does not meet maximum length of "+e.maxLength}),i},f.minItems=function(t,e,r,n){if(!(t instanceof Array))return null;var i=new s(t,e,r,n);return t.length>=e.minItems||i.addError({name:"minItems",argument:e.minItems,message:"does not meet minimum length of "+e.minItems}),i},f.maxItems=function(t,e,r,n){if(!(t instanceof Array))return null;var i=new s(t,e,r,n);return t.length<=e.maxItems||i.addError({name:"maxItems",argument:e.maxItems,message:"does not meet maximum length of "+e.maxItems}),i},f.uniqueItems=function(t,e,r,n){function i(t,e,r){for(var n=e+1;n"||a;o.addError({name:"not",argument:s,message:"is of prohibited type "+s})}}),o):null},t.exports=c},function(t,e,r){"use strict";var n=t.exports.Validator=r(61);t.exports.ValidatorResult=r(8).ValidatorResult,t.exports.ValidationError=r(8).ValidationError,t.exports.SchemaError=r(8).SchemaError,t.exports.validate=function(t,e,r){var i=new n;return i.validate(t,e,r)}},function(t,e,r){"use strict";var n=r(52),i=r(59),o=r(8),a=o.ValidatorResult,s=o.SchemaError,u=o.SchemaContext,c=function t(){this.customFormats=Object.create(t.prototype.customFormats),this.schemas={},this.unresolvedRefs=[],this.types=Object.create(f),this.attributes=Object.create(i.validators)};c.prototype.customFormats={},c.prototype.schemas=null,c.prototype.types=null,c.prototype.attributes=null,c.prototype.unresolvedRefs=null,c.prototype.addSchema=function(t,e){if(!t)return null;var r=e||t.id;return this.addSubSchema(r,t),r&&(this.schemas[r]=t),this.schemas[r]},c.prototype.addSubSchema=function(t,e){if(e&&"object"==typeof e){if(e.$ref){var r=n.resolve(t,e.$ref);return void(void 0===this.schemas[r]&&(this.schemas[r]=null,this.unresolvedRefs.push(r)))}var i=e.id&&n.resolve(t,e.id),a=i||t;if(i){if(this.schemas[i]){if(!o.deepCompareStrict(this.schemas[i],e))throw new Error("Schema <"+e+"> already exists with different definition");return this.schemas[i]}this.schemas[i]=e;var s=i.replace(/^([^#]*)#$/,"$1");this.schemas[s]=e}return this.addSubSchemaArray(a,e.items instanceof Array?e.items:[e.items]),this.addSubSchemaArray(a,e.extends instanceof Array?e.extends:[e.extends]),this.addSubSchema(a,e.additionalItems),this.addSubSchemaObject(a,e.properties),this.addSubSchema(a,e.additionalProperties),this.addSubSchemaObject(a,e.definitions),this.addSubSchemaObject(a,e.patternProperties),this.addSubSchemaObject(a,e.dependencies),this.addSubSchemaArray(a,e.disallow),this.addSubSchemaArray(a,e.allOf),this.addSubSchemaArray(a,e.anyOf),this.addSubSchemaArray(a,e.oneOf),this.addSubSchema(a,e.not),this.schemas[i]}},c.prototype.addSubSchemaArray=function(t,e){if(e instanceof Array)for(var r=0;r",t);var c=o.objectGetPath(r.schemas[u],a.substr(1));if(void 0===c)throw new s("no such schema "+a+" located in <"+u+">",t);return{subschema:c,switchSchema:e}},c.prototype.testType=function(t,e,r,n,i){if("function"==typeof this.types[i])return this.types[i].call(this,t);if(i&&"object"==typeof i){var o=this.validateSchema(t,i,r,n);return void 0===o||!(o&&o.errors.length)}return!0};var f=c.prototype.types={};f.string=function(t){return"string"==typeof t},f.number=function(t){return"number"==typeof t&&isFinite(t)},f.integer=function(t){return"number"==typeof t&&t%1===0},f.boolean=function(t){return"boolean"==typeof t},f.array=function(t){return t instanceof Array},f.null=function(t){return null===t},f.date=function(t){return t instanceof Date},f.any=function(t){return!0},f.object=function(t){return t&&"object"==typeof t&&!(t instanceof Array)&&!(t instanceof Date)},t.exports=c},function(t,e,r){!function(){function e(t,e){for(var r,n=t.length,i=e^n,o=0;n>=4;)r=255&t.charCodeAt(o)|(255&t.charCodeAt(++o))<<8|(255&t.charCodeAt(++o))<<16|(255&t.charCodeAt(++o))<<24,r=1540483477*(65535&r)+((1540483477*(r>>>16)&65535)<<16),r^=r>>>24,r=1540483477*(65535&r)+((1540483477*(r>>>16)&65535)<<16),i=1540483477*(65535&i)+((1540483477*(i>>>16)&65535)<<16)^r,n-=4,++o;switch(n){case 3:i^=(255&t.charCodeAt(o+2))<<16;case 2:i^=(255&t.charCodeAt(o+1))<<8;case 1:i^=255&t.charCodeAt(o),i=1540483477*(65535&i)+((1540483477*(i>>>16)&65535)<<16)}return i^=i>>>13,i=1540483477*(65535&i)+((1540483477*(i>>>16)&65535)<<16),i^=i>>>15,i>>>0}function r(t,e){var r,n,i,o,a,s,u,c;for(r=3&t.length,n=t.length-r,i=e,a=3432918353,s=461845907,c=0;c>>16)*a&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,i^=u,i=i<<13|i>>>19,o=5*(65535&i)+((5*(i>>>16)&65535)<<16)&4294967295,i=(65535&o)+27492+(((o>>>16)+58964&65535)<<16);switch(u=0,r){case 3:u^=(255&t.charCodeAt(c+2))<<16;case 2:u^=(255&t.charCodeAt(c+1))<<8;case 1:u^=255&t.charCodeAt(c),u=(65535&u)*a+(((u>>>16)*a&65535)<<16)&4294967295,u=u<<15|u>>>17,u=(65535&u)*s+(((u>>>16)*s&65535)<<16)&4294967295,i^=u}return i^=t.length,i^=i>>>16,i=2246822507*(65535&i)+((2246822507*(i>>>16)&65535)<<16)&4294967295,i^=i>>>13,i=3266489909*(65535&i)+((3266489909*(i>>>16)&65535)<<16)&4294967295,i^=i>>>16,i>>>0}var n=r;n.v2=e,n.v3=r;t.exports=n}()},function(t,e,r){var n=r(65);t.exports={evaluate:function(t,e){if(!t||0===t.length)return!0;if(!e)return!1;for(var r=!1,i=0;i-1}var i=r(11);t.exports=n},function(t,e,r){function n(t,e){var r=this.__data__,n=i(r,t);return n<0?r.push([t,e]):r[n][1]=e,this}var i=r(11);t.exports=n},function(t,e,r){function n(){this.__data__={hash:new i,map:new(a||o),string:new i}}var i=r(73),o=r(10),a=r(27);t.exports=n},function(t,e,r){function n(t){return i(this,t).delete(t)}var i=r(12);t.exports=n},function(t,e,r){function n(t){return i(this,t).get(t)}var i=r(12);t.exports=n},function(t,e,r){function n(t){return i(this,t).has(t)}var i=r(12);t.exports=n},function(t,e,r){function n(t,e){return i(this,t).set(t,e),this}var i=r(12);t.exports=n},function(t,e){function r(t){return this.__data__.set(t,n),this}var n="__lodash_hash_undefined__";t.exports=r},function(t,e){function r(t){return this.__data__.has(t)}t.exports=r},function(t,e,r){function n(){this.__data__=new i}var i=r(10);t.exports=n},function(t,e){function r(t){return this.__data__.delete(t)}t.exports=r},function(t,e){function r(t){return this.__data__.get(t)}t.exports=r},function(t,e){function r(t){return this.__data__.has(t)}t.exports=r},function(t,e,r){function n(t,e){var r=this.__data__;return r instanceof i&&r.__data__.length==a&&(r=this.__data__=new o(r.__data__)),r.set(t,e),this}var i=r(10),o=r(18),a=200;t.exports=n},function(t,e,r){var n=r(161),i=r(165),o=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(\.|\[\])(?:\4|$))/g,a=/\\(\\)?/g,s=n(function(t){var e=[];return i(t).replace(o,function(t,r,n,i){e.push(n?i.replace(a,"$1"):r||t)}),e});t.exports=s},function(t,e,r){function n(t){return i(t,!0,!0)}var i=r(86);t.exports=n},function(t,e,r){function n(t,e,r){var n=null==t?void 0:i(t,e);return void 0===n?r:n}var i=r(31);t.exports=n},function(t,e,r){function n(t,e){return null!=t&&o(t,e,i)}var i=r(92),o=r(123);t.exports=n},function(t,e){function r(t){return t}t.exports=r},function(t,e,r){function n(t){return o(t)&&i(t)}var i=r(23),o=r(9);t.exports=n},function(t,e,r){(function(t){var n=r(2),i=r(164),o="object"==typeof e&&e,a=o&&"object"==typeof t&&t,s=a&&a.exports===o,u=s?n.Buffer:void 0,c=u?function(t){return t instanceof u}:i;t.exports=c}).call(e,r(25)(t))},function(t,e,r){function n(t){return o(t)&&i(t.length)&&!!R[L.call(t)]; 4 | }var i=r(16),o=r(9),a="[object Arguments]",s="[object Array]",u="[object Boolean]",c="[object Date]",f="[object Error]",l="[object Function]",p="[object Map]",h="[object Number]",d="[object Object]",v="[object RegExp]",m="[object Set]",g="[object String]",y="[object WeakMap]",E="[object ArrayBuffer]",_="[object DataView]",b="[object Float32Array]",I="[object Float64Array]",O="[object Int8Array]",x="[object Int16Array]",A="[object Int32Array]",j="[object Uint8Array]",N="[object Uint8ClampedArray]",w="[object Uint16Array]",S="[object Uint32Array]",R={};R[b]=R[I]=R[O]=R[x]=R[A]=R[j]=R[N]=R[w]=R[S]=!0,R[a]=R[s]=R[E]=R[u]=R[_]=R[c]=R[f]=R[l]=R[p]=R[h]=R[d]=R[v]=R[m]=R[g]=R[y]=!1;var T=Object.prototype,L=T.toString;t.exports=n},function(t,e,r){var n=r(114),i=n(function(t,e,r){t[r]=e});t.exports=i},function(t,e,r){function n(t,e){if("function"!=typeof t||e&&"function"!=typeof e)throw new TypeError(o);var r=function(){var n=arguments,i=e?e.apply(this,n):n[0],o=r.cache;if(o.has(i))return o.get(i);var a=t.apply(this,n);return r.cache=o.set(i,a),a};return r.cache=new(n.Cache||i),r}var i=r(18),o="Expected a function";n.Cache=i,t.exports=n},function(t,e,r){function n(t){return a(t)?i(s(t)):o(t)}var i=r(34),o=r(100),a=r(13),s=r(15);t.exports=n},function(t,e){function r(){return[]}t.exports=r},function(t,e){function r(){return!1}t.exports=r},function(t,e,r){function n(t){return null==t?"":i(t)}var i=r(102);t.exports=n},function(t,e){function r(){throw new Error("setTimeout has not been defined")}function n(){throw new Error("clearTimeout has not been defined")}function i(t){if(f===setTimeout)return setTimeout(t,0);if((f===r||!f)&&setTimeout)return f=setTimeout,setTimeout(t,0);try{return f(t,0)}catch(e){try{return f.call(null,t,0)}catch(e){return f.call(this,t,0)}}}function o(t){if(l===clearTimeout)return clearTimeout(t);if((l===n||!l)&&clearTimeout)return l=clearTimeout,clearTimeout(t);try{return l(t)}catch(e){try{return l.call(null,t)}catch(e){return l.call(this,t)}}}function a(){v&&h&&(v=!1,h.length?d=h.concat(d):m=-1,d.length&&s())}function s(){if(!v){var t=i(a);v=!0;for(var e=d.length;e;){for(h=d,d=[];++m1)for(var r=1;r1&&(n=r[0]+"@",t=r[1]),t=t.replace(T,".");var i=t.split("."),o=s(i,e).join(".");return n+o}function c(t){for(var e,r,n=[],i=0,o=t.length;i=55296&&e<=56319&&i65535&&(t-=65536,e+=D(t>>>10&1023|55296),t=56320|1023&t),e+=D(t)}).join("")}function l(t){return t-48<10?t-22:t-65<26?t-65:t-97<26?t-97:b}function p(t,e){return t+22+75*(t<26)-((0!=e)<<5)}function h(t,e,r){var n=0;for(t=r?C(t/A):t>>1,t+=C(t/e);t>V*O>>1;n+=b)t=C(t/V);return C(n+(V+1)*t/(t+x))}function d(t){var e,r,n,i,o,s,u,c,p,d,v=[],m=t.length,g=0,y=N,E=j;for(r=t.lastIndexOf(w),r<0&&(r=0),n=0;n=128&&a("not-basic"),v.push(t.charCodeAt(n));for(i=r>0?r+1:0;i=m&&a("invalid-input"),c=l(t.charCodeAt(i++)),(c>=b||c>C((_-g)/s))&&a("overflow"),g+=c*s,p=u<=E?I:u>=E+O?O:u-E,!(cC(_/d)&&a("overflow"),s*=d;e=v.length+1,E=h(g-o,e,0==o),C(g/e)>_-y&&a("overflow"),y+=C(g/e),g%=e,v.splice(g++,0,y)}return f(v)}function v(t){var e,r,n,i,o,s,u,f,l,d,v,m,g,y,E,x=[];for(t=c(t),m=t.length,e=N,r=0,o=j,s=0;s=e&&vC((_-r)/g)&&a("overflow"),r+=(u-e)*g,e=u,s=0;s_&&a("overflow"),v==e){for(f=r,l=b;d=l<=o?I:l>=o+O?O:l-o,!(f= 0x80 (not a basic code point)","invalid-input":"Invalid input"},V=b-I,C=Math.floor,D=String.fromCharCode;E={version:"1.4.1",ucs2:{decode:c,encode:f},decode:d,encode:v,toASCII:g,toUnicode:m},i=function(){return E}.call(e,r,e,t),!(void 0!==i&&(t.exports=i))}(this)}).call(e,r(25)(t),r(17))},function(t,e){"use strict";function r(t,e){return Object.prototype.hasOwnProperty.call(t,e)}t.exports=function(t,e,i,o){e=e||"&",i=i||"=";var a={};if("string"!=typeof t||0===t.length)return a;var s=/\+/g;t=t.split(e);var u=1e3;o&&"number"==typeof o.maxKeys&&(u=o.maxKeys);var c=t.length;u>0&&c>u&&(c=u);for(var f=0;f=0?(l=v.substr(0,m),p=v.substr(m+1)):(l=v,p=""),h=decodeURIComponent(l),d=decodeURIComponent(p),r(a,h)?n(a[h])?a[h].push(d):a[h]=[a[h],d]:a[h]=d}return a};var n=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)}},function(t,e){"use strict";function r(t,e){if(t.map)return t.map(e);for(var r=[],n=0;n 23 | 24 | { reactRoutes } 25 | 26 | , 27 | rootElement 28 | ) 29 | -------------------------------------------------------------------------------- /src/client/optimizely_manager.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird' 2 | import enums from '../common/utils/enums' 3 | import optimizely from '@optimizely/optimizely-sdk' 4 | import optimizelyLoggerFactory from '@optimizely/optimizely-sdk/lib/plugins/logger' 5 | 6 | const PROJECT_ID = enums.PROJECT_ID 7 | const PROJECT_JSON_URL = `https://cdn.optimizely.com/json/${PROJECT_ID}.json` 8 | 9 | // Singleton instance of the optimizely object 10 | var optlyInstance; 11 | 12 | // In-memory copy of the datafile. We could also keep this in some form of cache like redis or memcached 13 | var datafile; 14 | 15 | module.exports = { 16 | /** 17 | * Get the singleton instance. 18 | * @return {Object} the optimizely instance 19 | */ 20 | getInstance(fetchDatafile) { 21 | return new Promise((resolve, reject) => { 22 | // check if we have a datafile or if we are forced to re-fetch it 23 | if (!datafile || fetchDatafile) { 24 | getDatafile() 25 | .then((fetchedDatafile) => { 26 | datafile = fetchedDatafile 27 | var instance = _getInstance(fetchedDatafile) 28 | resolve(instance) 29 | }) 30 | } else { 31 | var instance = _getInstance(datafile) 32 | resolve(instance) 33 | } 34 | }); 35 | } 36 | } 37 | 38 | function _getInstance(datafile) { 39 | if (!optlyInstance) { 40 | optlyInstance = optimizely.createInstance({ 41 | datafile, 42 | logger: optimizelyLoggerFactory.createLogger({ 43 | logLevel: 2, 44 | }), 45 | skipJSONValidation: true, // This should be set to false if we modify the datafile in any way 46 | }) 47 | } 48 | return optlyInstance 49 | } 50 | 51 | function getDatafile() { 52 | return new Promise((resolve, reject) => { 53 | // Try to grab it from the global var before attempting an XHR request. 54 | // @NOTE: In a prod environment we probably don't want to do this and instead 55 | // grab it from the CDN. It can be a non-blocking deferred request though 56 | // because we've got server-side rendering. 57 | if (window.__OPTIMIZELY_DATAFILE__) { 58 | resolve(window.__OPTIMIZELY_DATAFILE__) 59 | } else { 60 | fetch(PROJECT_JSON_URL, { mode: 'cors' }) 61 | .then((response) => { 62 | resolve(response.json()) 63 | }) 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/common/action_creators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These action creators are only used client-side to react to user 3 | * interactions. 4 | */ 5 | import { 6 | ADD_TO_CART, 7 | CLEAR_CART, 8 | GET_CHECKOUT_FLOW, 9 | } from '../actions' 10 | import enums from '../utils/enums' 11 | import optimizelyManager from '../../client/optimizely_manager' 12 | 13 | function addToCartSuccess(item) { 14 | return { 15 | type: ADD_TO_CART, 16 | item, 17 | } 18 | } 19 | 20 | function getCheckoutFlowSuccess(checkoutFlowVariation) { 21 | return { 22 | type: GET_CHECKOUT_FLOW, 23 | checkoutFlowVariation, 24 | } 25 | } 26 | 27 | function completeCheckoutSuccess() { 28 | return { 29 | type: CLEAR_CART, 30 | } 31 | } 32 | 33 | /** 34 | * Add the given item to the user's cart 35 | * @param {string} userId We need the user id to track a conversion event 36 | * @param {Object} item The item we are adding to the cart 37 | * @return {Function} 38 | */ 39 | export function addToCart(item) { 40 | return function(dispatch, getState) { 41 | const userId = getState().currentUserId 42 | 43 | optimizelyManager.getInstance() 44 | .then((optimizelyInstance) => { 45 | optimizelyInstance.track(enums.EVENT_KEYS.ADD_TO_CART, userId) 46 | dispatch(addToCartSuccess(item)) 47 | }) 48 | } 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | export function fetchItem(itemId) { 55 | return function(dispatch, getState) { 56 | // @TODO(mng): implement 57 | } 58 | } 59 | 60 | /** 61 | * Gets the checkout flow to use for checkout 62 | * @param {string} userId User id needed to bucket the user and get the variation key 63 | * @return {Function} 64 | */ 65 | export function getCheckoutFlow() { 66 | return function(dispatch, getState) { 67 | const userId = getState().currentUserId 68 | optimizelyManager.getInstance() 69 | .then((optimizelyInstance) => { 70 | const checkoutFlowVariation = optimizelyInstance.getVariation( 71 | enums.EXPERIMENT_KEYS.CHECKOUT_FLOW_EXPERIMENT, 72 | userId 73 | ) 74 | 75 | dispatch(getCheckoutFlowSuccess(checkoutFlowVariation)) 76 | }) 77 | } 78 | } 79 | 80 | /** 81 | * Activates the user in the checkout flow experiment 82 | * This tracks an impression event for the experiment 83 | * @param {string} userId 84 | * @return {Function} 85 | */ 86 | export function activateCheckoutFlow() { 87 | return function(dispatch, getState) { 88 | const userId = getState().currentUserId 89 | optimizelyManager.getInstance() 90 | .then((optimizelyInstance) => { 91 | const checkoutFlowVariation = optimizelyInstance.activate( 92 | enums.EXPERIMENT_KEYS.CHECKOUT_FLOW_EXPERIMENT, 93 | userId 94 | ) 95 | 96 | dispatch(getCheckoutFlowSuccess(checkoutFlowVariation)) 97 | }) 98 | } 99 | } 100 | 101 | /** 102 | * Tracks a checkout complete event for the given user 103 | * @param {string} userId 104 | * @return {Function} 105 | */ 106 | export function completeCheckout() { 107 | return function(dispatch, getState) { 108 | const userId = getState().currentUserId 109 | return new Promise((resolve, reject) => { 110 | // compute the checkout total to send to the event 111 | const cart = getState().cart 112 | const itemsInCart = _.filter(getState().items, (item) => { 113 | return cart.indexOf(item.id) !== -1 114 | }) 115 | const checkoutTotal = itemsInCart.reduce((subtotal, item) => { 116 | return subtotal + item.price 117 | }, 0) 118 | 119 | optimizelyManager.getInstance() 120 | .then((optimizelyInstance) => { 121 | optimizelyInstance.track(enums.EVENT_KEYS.CHECKOUT_COMPLETE, userId, null, checkoutTotal * 100) 122 | dispatch(completeCheckoutSuccess()) 123 | resolve() 124 | }) 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/common/actions/index.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_CART = 'ADD_TO_CART' 2 | export const CLEAR_CART = 'CLEAR_CART' 3 | export const FETCH_ITEMS = 'FETCH_ITEMS' 4 | export const GET_CHECKOUT_FLOW = 'GET_CHECKOUT_FLOW' 5 | -------------------------------------------------------------------------------- /src/common/components/cart/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import { Link } from 'react-router' 5 | import enums from '../../utils/enums' 6 | import React from 'react' 7 | import * as ShoeActions from '../../action_creators' 8 | 9 | function mapStateToProps(state) { 10 | const items = _.filter(state.items, (item) => { 11 | return state.cart.indexOf(item.id) !== -1 12 | }) 13 | 14 | return { 15 | items, 16 | checkoutFlow: state.optimizelyExperimentData[enums.EXPERIMENT_KEYS.CHECKOUT_FLOW_EXPERIMENT], 17 | } 18 | } 19 | 20 | function mapDispatchToProps(dispatch) { 21 | return bindActionCreators(ShoeActions, dispatch) 22 | } 23 | 24 | class Cart extends React.Component { 25 | componentDidMount() { 26 | this.props.getCheckoutFlow() 27 | } 28 | 29 | render() { 30 | // determine whether to show one step or two step checkout when the user clicks on the checkout button 31 | const checkoutLink = 32 | this.props.checkoutFlow === enums.VARIATION_KEYS.TWO_STEP_CHECKOUT ? 33 | '/checkout/shipping' : '/checkout' 34 | const items = this.props.items 35 | const subtotal = items.reduce((subtotal, item) => { 36 | return subtotal + item.price 37 | }, 0) 38 | 39 | return ( 40 |
41 |
42 |
43 |
44 |
YOUR CART ({ items.length })
45 |
46 |
47 |
    48 | { 49 | items.map((item) => { 50 | return ( 51 |
  • 52 |
    53 |
    54 | 55 |
    56 |
    57 | { item.name } 58 | ${ item.price } 59 |
    60 |
    61 |
  • 62 | ) 63 | }) 64 | } 65 |
66 |
67 |
68 |
69 |
70 |
SUMMARY
71 |
72 | Subtotal 73 | ${ subtotal } 74 |
75 |
76 |
77 | Total 78 | ${ subtotal } 79 |
80 |
81 | 82 | 83 | 84 |
85 |
86 |
87 |
88 |
89 | ) 90 | } 91 | } 92 | 93 | export default connect(mapStateToProps, mapDispatchToProps)(Cart) 94 | -------------------------------------------------------------------------------- /src/common/components/checkout/common/address_form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default (props) => { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/common/components/checkout/common/credit_card_form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default (props) => { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/common/components/checkout/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import React from 'react' 4 | import * as ShoeActions from '../../action_creators' 5 | 6 | function mapStateToProps(state, ownProps) { 7 | // add state here 8 | return {} 9 | } 10 | 11 | function mapDispatchToProps(dispatch) { 12 | return bindActionCreators(ShoeActions, dispatch) 13 | } 14 | 15 | class CheckoutPage extends React.Component { 16 | componentDidMount() { 17 | // activate the user (track an impression if not already tracked) 18 | this.props.activateCheckoutFlow() 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |

Checkout

25 | { this.props.children } 26 |
27 | ) 28 | } 29 | } 30 | 31 | export default connect(mapStateToProps, mapDispatchToProps)(CheckoutPage) 32 | -------------------------------------------------------------------------------- /src/common/components/checkout/one_step_checkout/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import AddressForm from '../common/address_form' 4 | import CreditCardForm from '../common/credit_card_form' 5 | import enums from '../../../utils/enums' 6 | import React from 'react' 7 | import * as ShoeActions from '../../../action_creators' 8 | 9 | function mapStateToProps(state, ownProps) { 10 | return {} 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators(ShoeActions, dispatch) 15 | } 16 | 17 | class OneStepCheckoutPage extends React.Component { 18 | completeCheckout() { 19 | this.props.completeCheckout() 20 | .then(() => { 21 | this.context.router.push('/') 22 | }) 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |
29 | Shipping Address 30 |
31 | 32 |
33 |
34 | Credit Card Info 35 |
36 | 37 | 38 | 41 |
42 | ) 43 | } 44 | } 45 | 46 | OneStepCheckoutPage.contextTypes = { 47 | router: React.PropTypes.object.isRequired 48 | }; 49 | 50 | export default connect(mapStateToProps, mapDispatchToProps)(OneStepCheckoutPage) 51 | -------------------------------------------------------------------------------- /src/common/components/checkout/two_step_checkout/billing_info.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import CreditCardForm from '../common/credit_card_form' 4 | import React from 'react' 5 | import * as ShoeActions from '../../../action_creators' 6 | 7 | function mapStateToProps(state, ownProps) { 8 | // add state here 9 | return {} 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators(ShoeActions, dispatch) 14 | } 15 | 16 | class BillingInfoPage extends React.Component { 17 | completeCheckout() { 18 | this.props.completeCheckout() 19 | .then(() => { 20 | this.context.router.push('/') 21 | }) 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 |
28 | 2. Billing 29 |
30 | 31 | 36 |
37 | ) 38 | } 39 | } 40 | 41 | BillingInfoPage.contextTypes = { 42 | router: React.PropTypes.object.isRequired 43 | }; 44 | 45 | export default connect(mapStateToProps, mapDispatchToProps)(BillingInfoPage) 46 | -------------------------------------------------------------------------------- /src/common/components/checkout/two_step_checkout/shipping_address.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router' 2 | import AddressForm from '../common/address_form' 3 | import React from 'react' 4 | 5 | export default (props) => { 6 | return ( 7 |
8 |
9 | 1. Shipping 10 |
11 | 12 | 13 | 14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/common/components/home/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import React, { PropTypes } from 'react' 3 | import ItemList from './item_list' 4 | 5 | function mapStateToProps(state) { 6 | return { 7 | items: state.items 8 | } 9 | } 10 | 11 | class Home extends React.Component { 12 | render() { 13 | return ( 14 |
15 |
16 | 19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | export default connect(mapStateToProps)(Home) 26 | -------------------------------------------------------------------------------- /src/common/components/home/item_list.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ItemListItem from './item_list_item' 3 | 4 | export default class ShoeListComponent extends React.Component { 5 | render() { 6 | const items = this.props.items 7 | return ( 8 |
    9 | { 10 | Object.keys(items).map((itemId) => { 11 | return ( 12 | 16 | ) 17 | }) 18 | } 19 |
20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/components/home/item_list_item.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class ShoeListItem extends React.Component { 5 | render() { 6 | const item = this.props.item 7 | 8 | return ( 9 |
  • 10 | 11 | 15 |
    { item.name }
    16 |
    { item.category }
    17 |
    ${ item.price }
    18 | 19 |
  • 20 | ) 21 | } 22 | } 23 | 24 | const listImageStyle = { 25 | height: '100px', 26 | width: '100px', 27 | } 28 | 29 | const listItemStyle = { 30 | 'listStyle': 'none', 31 | 'width': '200px', 32 | 'float': 'left', 33 | 'font-family': 'sans-serif' 34 | } 35 | -------------------------------------------------------------------------------- /src/common/components/pdp/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import { Link } from 'react-router' 5 | import React, { PropTypes } from 'react' 6 | import * as ShoeActions from '../../action_creators' 7 | 8 | function mapStateToProps(state, ownProps) { 9 | const itemId = parseInt(ownProps.params.id, 10) 10 | const item = _.find(state.items, (_item) => { 11 | return _item.id === itemId 12 | }) 13 | 14 | return { 15 | itemId, 16 | item, 17 | } 18 | } 19 | 20 | function mapDispatchToProps(dispatch) { 21 | return bindActionCreators(ShoeActions, dispatch) 22 | } 23 | 24 | class ProducDetailPage extends React.Component { 25 | addToCart() { 26 | this.props.addToCart(this.props.item) 27 | this.context.router.push('/cart') 28 | } 29 | 30 | componentDidMount() { 31 | this.props.fetchShoe && this.props.fetchShoe(this.props.itemId) 32 | } 33 | 34 | render() { 35 | const item = this.props.item 36 | return ( 37 |
    38 |
    39 | 40 | Back to Product list 41 | 42 |
    43 |
    44 | 48 |
    { item.name }
    49 |
    { item.category }
    50 |
    ${ item.price }
    51 | 56 |
    57 |
    58 | ) 59 | } 60 | } 61 | 62 | ProducDetailPage.contextTypes = { 63 | router: React.PropTypes.object.isRequired 64 | }; 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(ProducDetailPage) 67 | 68 | const imageStyle = { 69 | height: '150px', 70 | width: '150px', 71 | } 72 | -------------------------------------------------------------------------------- /src/common/components/shared/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IndexLink } from 'react-router'; 3 | 4 | export default class Header extends React.Component { 5 | render() { 6 | return ( 7 |
    8 |

    9 | Attic and Button 10 |

    11 |
    12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/common/containers/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../components/shared/header' 3 | 4 | export default class App extends React.Component { 5 | render() { 6 | return ( 7 |
    8 |
    9 | { this.props.children } 10 |
    11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/reducers/cart.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART 3 | } from '../actions' 4 | 5 | const initialState = [] 6 | 7 | export default function items(state = initialState, action) { 8 | switch (action.type) { 9 | case ADD_TO_CART: 10 | const cartItems = [...state] 11 | cartItems.push(action.item.id) 12 | 13 | return cartItems 14 | default: 15 | return state 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import cart from './cart' 3 | import optimizelyExperimentData from './optimizely_experiment_data' 4 | import items from './items' 5 | 6 | const rootReducer = combineReducers({ 7 | cart, 8 | currentUserId: (state = {}) => state, 9 | optimizelyExperimentData, 10 | items, 11 | }) 12 | 13 | export default rootReducer 14 | -------------------------------------------------------------------------------- /src/common/reducers/items.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_ITEMS 3 | } from '../actions' 4 | 5 | export default function items(state = {}, action) { 6 | switch (action.type) { 7 | case FETCH_ITEMS: 8 | return action.payload 9 | default: 10 | return state 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/reducers/optimizely_experiment_data.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CHECKOUT_FLOW 3 | } from '../actions' 4 | import enums from '../utils/enums' 5 | 6 | const initialState = {} 7 | 8 | export default function optimizelyExperimentData(state = initialState, action) { 9 | switch (action.type) { 10 | case GET_CHECKOUT_FLOW: 11 | return { 12 | ...state, 13 | [enums.EXPERIMENT_KEYS.CHECKOUT_FLOW_EXPERIMENT]: action.checkoutFlowVariation, 14 | } 15 | } 16 | return initialState 17 | } 18 | -------------------------------------------------------------------------------- /src/common/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IndexRoute, Route } from 'react-router' 3 | 4 | import App from '../containers/app' 5 | 6 | import Cart from '../components/cart' 7 | import BillingPage from '../components/checkout/two_step_checkout/billing_info' 8 | import Checkout from '../components/checkout' 9 | import Home from '../components/home' 10 | import OneStepCheckout from '../components/checkout/one_step_checkout' 11 | import ProductDetailPage from '../components/pdp' 12 | import ShippingPage from '../components/checkout/two_step_checkout/shipping_address' 13 | 14 | export const reactRoutes = 15 | ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /src/common/store/configure_store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | 5 | export default function configureStore(preloadedState) { 6 | const store = createStore( 7 | rootReducer, 8 | preloadedState, 9 | applyMiddleware(thunk) 10 | ) 11 | 12 | if (module.hot) { 13 | // Enable Webpack hot module replacement for reducers 14 | module.hot.accept('../reducers', () => { 15 | const nextRootReducer = require('../reducers').default 16 | store.replaceReducer(nextRootReducer) 17 | }) 18 | } 19 | 20 | return store 21 | } 22 | -------------------------------------------------------------------------------- /src/common/utils/enums.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global enums 3 | */ 4 | export default { 5 | PROJECT_ID: 7499963221, // @TODO: replace with your own project ID 6 | EXPERIMENT_KEYS: { 7 | SORTING_EXPERIMENT: 'sorting_experiment', 8 | CHECKOUT_FLOW_EXPERIMENT: 'checkout_flow_experiment', 9 | }, 10 | VARIATION_KEYS: { 11 | SORT_BY_PRICE: 'sort_by_price', 12 | SORT_BY_NAME: 'sort_by_name', 13 | TWO_STEP_CHECKOUT: 'two_step_checkout', 14 | ONE_STEP_CHECKOUT: 'one_step_checkout', 15 | }, 16 | EVENT_KEYS: { 17 | ADD_TO_CART: 'add_to_cart', 18 | CHECKOUT_COMPLETE: 'checkout_complete', 19 | }, 20 | ROUTES: { 21 | HOME: 'home', 22 | CHECKOUT: 'checkout', 23 | }, 24 | COOKIE_KEYS: { 25 | USER: 'item_shop_user', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /src/common/utils/optimizely_manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper around the server side SDK. 3 | * Manages the optimizely instance as well as the datafile download. 4 | */ 5 | import Promise from 'bluebird' 6 | import enums from './enums' 7 | import optimizely from '@optimizely/optimizely-sdk' 8 | import optimizelyLoggerFactory from '@optimizely/optimizely-sdk/lib/plugins/logger' 9 | import requestPromise from 'request-promise' 10 | 11 | const PROJECT_ID = enums.PROJECT_ID 12 | const PROJECT_JSON_URL = `https://cdn.optimizely.com/json/${PROJECT_ID}.json` 13 | 14 | // Singleton instance of the optimizely object 15 | var optlyInstance; 16 | 17 | // In-memory copy of the datafile. We could also keep this in some form of cache like redis or memcached 18 | var datafile; 19 | 20 | module.exports = { 21 | /** 22 | * Get the singleton instance. 23 | * @return {Object} the optimizely instance 24 | */ 25 | getInstance(fetchDatafile) { 26 | return new Promise((resolve, reject) => { 27 | // check if we have a datafile or if we are forced to re-fetch it 28 | if (!datafile || fetchDatafile) { 29 | getDatafile() 30 | .then((fetchedDatafile) => { 31 | datafile = fetchedDatafile 32 | var instance = _getInstance(fetchedDatafile) 33 | resolve(instance) 34 | }) 35 | } else { 36 | var instance = _getInstance(datafile) 37 | resolve(instance) 38 | } 39 | }); 40 | }, 41 | 42 | /** 43 | * Returns the cached datafile 44 | * @return {Object} 45 | */ 46 | getDatafile() { 47 | return datafile 48 | } 49 | } 50 | 51 | function _getInstance(datafile) { 52 | if (!optlyInstance) { 53 | optlyInstance = optimizely.createInstance({ 54 | datafile, 55 | logger: optimizelyLoggerFactory.createLogger({ 56 | logLevel: 2, 57 | }), 58 | skipJSONValidation: true, // This should be set to false if we modify the datafile in any way 59 | }) 60 | } 61 | return optlyInstance 62 | } 63 | 64 | function getDatafile() { 65 | return requestPromise({ 66 | url: PROJECT_JSON_URL, 67 | json: true, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Hapi from 'hapi' 2 | import Inert from 'inert' 3 | import path from 'path' 4 | 5 | const server = new Hapi.Server({ 6 | connections: { 7 | routes: { 8 | cors: { 9 | origin: ['*'], 10 | }, 11 | files: { 12 | relativeTo: path.join(__dirname, '../public'), 13 | }, 14 | }, 15 | }, 16 | }); 17 | 18 | server.connection({ 19 | host: 'localhost', 20 | port: '4242', 21 | }); 22 | 23 | const plugins = [ 24 | Inert, 25 | require('./server/routes/index'), 26 | ]; 27 | 28 | server.register(plugins, function(err) { 29 | if (err) { 30 | throw err; 31 | } 32 | }); 33 | 34 | module.exports = { 35 | server, 36 | }; 37 | -------------------------------------------------------------------------------- /src/server/routes/api.js: -------------------------------------------------------------------------------- 1 | import { COOKIE_KEYS } from '../../common/utils/enums' 2 | import cartService from '../services/cart' 3 | import itemService from '../services/items' 4 | 5 | /** 6 | * Add the given product to the user's cart 7 | * @param {Object} request 8 | * @param {Function} reply 9 | */ 10 | function addToCartHandler(request, reply) { 11 | const user = request.yar.get(COOKIE_KEYS.USER) 12 | const payload = request.payload 13 | const productId = payload.productId 14 | const quantity = payload.quantity 15 | cartService.addToCart(user.key, productId, quantity) 16 | reply({ 17 | success: true, 18 | }); 19 | } 20 | 21 | /** 22 | * Get the user's cart 23 | * @param {Object} request 24 | * @param {Function} reply 25 | */ 26 | function getCartHandler(request, reply) { 27 | const user = request.yar.get(COOKIE_KEYS.USER) 28 | const cart = cartService.getCart(user.key) 29 | reply(cart) 30 | } 31 | 32 | /** 33 | * Get the item by id 34 | * @param {Object} request 35 | * @param {Function} reply 36 | */ 37 | function itemHandler(request, reply) { 38 | const itemId = request.params.id 39 | const item = cartService.getShoe(itemId) 40 | reply(item) 41 | } 42 | 43 | /** 44 | * Routes API calls 45 | * @type {Array} 46 | */ 47 | function itemsHandler(request, reply) { 48 | const items = itemService.getShoes() 49 | return reply(items) 50 | } 51 | 52 | export default [ 53 | { 54 | method: 'GET', 55 | path: '/api/cart', 56 | handle: getCartHandler, 57 | }, 58 | { 59 | method: 'POST', 60 | path: '/api/cart', 61 | handler: addToCartHandler, 62 | }, 63 | { 64 | method: 'GET', 65 | path: '/api/item/{id}', 66 | handler: itemHandler, 67 | }, 68 | { 69 | method: 'GET', 70 | path: '/api/items', 71 | handler: itemsHandler, 72 | }, 73 | ]; 74 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | import vision from 'vision' 2 | import yar from 'yar' 3 | 4 | import staticRoutes from './static' 5 | import viewRoutes from './views' 6 | 7 | const register = function(server, options, next) { 8 | server.register([vision, { 9 | register: yar, 10 | options: { 11 | storeBlank: false, 12 | cookieOptions: { 13 | password: 'the-password-must-be-at-least-32', 14 | isSecure: false 15 | } 16 | } 17 | }], function(err) { 18 | if (err) { 19 | throw err 20 | } 21 | 22 | const routes = [].concat(staticRoutes, viewRoutes) 23 | server.route(routes) 24 | }) 25 | 26 | next() 27 | } 28 | 29 | register.attributes = { 30 | name: 'default-routes', 31 | } 32 | 33 | module.exports = register 34 | -------------------------------------------------------------------------------- /src/server/routes/static.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Routes for serving static assets 3 | * @type {Array} 4 | */ 5 | export default [ 6 | { 7 | method: 'GET', 8 | path: '/images/{param*}', 9 | handler: { 10 | directory: { 11 | path: '.', 12 | redirectToSlash: true, 13 | index: true 14 | } 15 | } 16 | }, 17 | { 18 | method: 'GET', 19 | path: '/public/{param*}', 20 | handler: { 21 | directory: { 22 | path: '.', 23 | index: false, 24 | listing: false, 25 | } 26 | } 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /src/server/routes/views.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { match } from 'react-router'; 3 | import { reactRoutes } from '../../common/routes' 4 | import enums from '../../common/utils/enums' 5 | import optimizelyManager from '../../common/utils/optimizely_manager' 6 | import { 7 | renderFullPage, 8 | renderReactApp, 9 | } from '../services/page_template' 10 | import itemService from '../services/items' 11 | import uuid from 'uuid-v4' 12 | 13 | /** 14 | * 15 | * Handles rendering the app's views using React 16 | * This route is a catch all route and will catch everything that 17 | * doesn't hit the /api or /public paths 18 | * @param {Object} request 19 | * @param {Object} reply 20 | */ 21 | const viewHandler = function(request, reply) { 22 | // create a session and store in the user's cookie 23 | // we use this session value as the user id to activate and track our experiments 24 | let user = request.yar.get('attic_and_button_shop_user') 25 | if (!user) { 26 | // create a user id for unknown users and use that id for subsequent sessions 27 | user = { key: uuid() } 28 | request.yar.set('attic_and_button_shop_user', user) 29 | } 30 | 31 | // this is used by our react router to determine which component to render 32 | const location = request.url.path 33 | 34 | // Activate the user for the sorting experiment 35 | optimizelyManager.getInstance() 36 | .then((optimizelyInstance) => { 37 | const variation = optimizelyInstance.activate(enums.EXPERIMENT_KEYS.SORTING_EXPERIMENT, user.key) 38 | 39 | // Use the variation key to determine the property we are sorting on 40 | const sortBy = variation === enums.VARIATION_KEYS.SORT_BY_PRICE ? 'price' : 'name' 41 | const data = itemService.getItems(sortBy) 42 | 43 | // Match the routes using the react router, which determines which component to render based on the route 44 | match( 45 | { 46 | routes: reactRoutes, 47 | location, 48 | }, 49 | (err, redirectLocation, renderProps) => { 50 | if (err) { 51 | console.log(err); 52 | } 53 | 54 | if (renderProps) { 55 | // Compile an initial state for our store so we can render on the server side 56 | const preloadedState = { 57 | currentUserId: user.key, 58 | optimizelyExperimentData: {}, 59 | items: data, 60 | } 61 | 62 | // conditionally activate the checkout flow experiment if the user is in the checkout page 63 | if (location === enums.ROUTES.CHECKOUT) { 64 | const checkoutFlowVariation = optimizelyInstance.activate(enums.EXPERIMENT_KEYS.CHECKOUT_FLOW_EXPERIMENT, user) 65 | preloadedState.optimizelyExperimentData[enums.EXPERIMENT_KEYS.CHECKOUT_FLOW_EXPERIMENT] = checkoutFlowVariation 66 | } 67 | 68 | // Render our React App 69 | const html = renderReactApp(renderProps, preloadedState) 70 | 71 | // Render React App with the rest of the HTML body 72 | const fullHtml = renderFullPage( 73 | { 74 | datafile: optimizelyManager.getDatafile(), 75 | html, 76 | preloadedState, 77 | variation, 78 | } 79 | ) 80 | 81 | return reply(fullHtml).type('text/html') 82 | } 83 | } 84 | ) 85 | }) 86 | } 87 | 88 | module.exports = [ 89 | { 90 | method: 'GET', 91 | path: '/{path*}', 92 | handler: viewHandler, 93 | }, 94 | ] 95 | -------------------------------------------------------------------------------- /src/server/services/cart.js: -------------------------------------------------------------------------------- 1 | const userCartMapping = {}; 2 | 3 | /** 4 | * Stub cart service. 5 | * Stores everything in memory at the moment. 6 | * @type {Object} 7 | */ 8 | module.exports = { 9 | /** 10 | * Add product to the user's cart 11 | * @param {string} userId 12 | * @param {string} productId 13 | * @param {Number} quantity 14 | */ 15 | addToCart: (userId, productId, quantity) => { 16 | if (userCartMapping.hasOwnProperty(userId)) { 17 | let cart = userCartMapping[userId]; 18 | cart.push({ 19 | productId, 20 | quantity, 21 | }); 22 | } else { 23 | userCartMapping[userId] = [ 24 | { 25 | productId, 26 | quantity, 27 | } 28 | ]; 29 | } 30 | }, 31 | 32 | /** 33 | * Get the given user's cart 34 | * @param {string} userId 35 | * @return {Object} 36 | */ 37 | getCart: (userId) => { 38 | return userCartMapping[userId]; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/server/services/items.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const itemsData = [ 4 | { id: 1, name: 'Long Sleeve Swing Shirt', 'color': 'Baby Blue', category: 'Shirts', price: 54, image_url: 'item_7.png' }, 5 | { id: 2, name: 'Bo Henry', 'color': 'Khaki', category: 'Shorts', price: 37, image_url: 'item_2.png' }, 6 | { id: 3, name: 'The "Go" Bag', 'color': 'Forest Green', category: 'Bags', price: 118, image_url: 'item_3.png' }, 7 | { id: 4, name: 'Springtime', 'color': 'Rose', category: 'Dresses', price: 84, image_url: 'item_4.png' }, 8 | { id: 5, name: 'The Night Out', 'color': 'Olive Green', category: 'Dresses', price: 153, image_url: 'item_5.png' }, 9 | { id: 6, name: 'Dawson Trolley', 'color': 'Pine Green', category: 'Shirts', price: 107, image_url: 'item_6.png' }, 10 | { id: 7, name: 'Derby Hat', 'color': 'White', category: 'Hats', price: 100, image_url: 'item_1.png' }, 11 | { id: 8, name: 'Long Sleever Tee', 'color': 'Baby Blue', category: 'Shirts', price: 62, image_url: 'item_8.png' }, 12 | { id: 9, name: 'Simple Cardigan', 'color': 'Olive Green', category: 'Sweaters', price: 238, image_url: 'item_9.png' }, 13 | ] 14 | 15 | module.exports = { 16 | /** 17 | * Get items and sort by the given property 18 | * @param {string} sortBy Determines the property to sort the items on 19 | * @return {Array} 20 | */ 21 | getItems: function(sortBy) { 22 | return _.chain(itemsData) 23 | .cloneDeep() 24 | .keyBy('id') 25 | .sortBy(sortBy) 26 | .value() 27 | }, 28 | 29 | /** 30 | * Get item by Id 31 | * @param {string} itemId 32 | * @return {Object} 33 | */ 34 | getItem: function(itemId) { 35 | return _.chain(itemsData) 36 | .cloneDeep() 37 | .find(item => item.id === itemId) 38 | .value() 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /src/server/services/page_template.js: -------------------------------------------------------------------------------- 1 | import { renderToString } from 'react-dom/server' 2 | import { RouterContext } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from '../../common/store/configure_store' 5 | import React from 'react' 6 | 7 | /** 8 | * Renders the React app 9 | * @param {Object} renderProps 10 | * @param {Object} state The state to populate our redux store with 11 | * @return {string} The rendered HTML 12 | */ 13 | export const renderReactApp = (renderProps, state) => { 14 | // Create a new Redux store instance 15 | const store = configureStore(state) 16 | const html = renderToString( 17 | 18 | 19 | 20 | ) 21 | return html 22 | } 23 | 24 | /** 25 | * Render the full HTML page to display to the user 26 | * @param {Object} data 27 | * @param {Object} data.html 28 | * @param {Object} data.datafile 29 | * @param {Object} data.preloadedState 30 | * @return {string} the fully render HTML 31 | */ 32 | export const renderFullPage = (data) => { 33 | return ` 34 | 35 | 36 | 37 | 38 | 39 | 40 | Attic and Button 41 | 42 | 43 |
    ${data.html}
    44 | 51 | 52 | 53 | 54 | ` 55 | } 56 | -------------------------------------------------------------------------------- /src/start.js: -------------------------------------------------------------------------------- 1 | // needed for babel to transpile the JSX 2 | require('babel-core/register')({ 3 | presets: ['es2015', 'react', 'stage-2'] 4 | }) 5 | 6 | const server = require('./').server; 7 | 8 | server.start((err) => { 9 | if (err) { 10 | throw err; 11 | } 12 | console.log('Server running at:', server.info.uri); 13 | }); 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | module: { 6 | loaders: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | loader: 'babel', // 'babel-loader' is also a legal name to reference 11 | query: { 12 | presets: ['babel-preset-es2015', 'babel-preset-react', 'babel-preset-stage-2'] 13 | } 14 | } 15 | ] 16 | }, 17 | plugins: [ 18 | new webpack.optimize.DedupePlugin(), 19 | ], 20 | entry: './src/client/index.js', 21 | output: { 22 | filename: 'main.js', 23 | path: './public' 24 | } 25 | } 26 | --------------------------------------------------------------------------------