├── Procfile ├── requirements.txt ├── images ├── logo.png ├── 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 └── screenshot.png ├── items.csv ├── .gitignore ├── test_webhook.py ├── LICENSE ├── templates ├── purchase.html └── index.html ├── application.py └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: python application.python 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | requests==2.6.2 3 | optimizely-sdk==3.2.0 4 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/item_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_1.png -------------------------------------------------------------------------------- /images/item_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_2.png -------------------------------------------------------------------------------- /images/item_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_3.png -------------------------------------------------------------------------------- /images/item_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_4.png -------------------------------------------------------------------------------- /images/item_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_5.png -------------------------------------------------------------------------------- /images/item_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_6.png -------------------------------------------------------------------------------- /images/item_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_7.png -------------------------------------------------------------------------------- /images/item_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_8.png -------------------------------------------------------------------------------- /images/item_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/item_9.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimizely/python-sdk-demo-app/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /items.csv: -------------------------------------------------------------------------------- 1 | Long Sleeve Swing Shirt,Baby Blue,Shirts,$54,item_7.png 2 | Bo Henry,Khaki,Shorts,$37,item_2.png 3 | The "Go" Bag,Forest Green,Bags,$118,item_3.png 4 | Springtime,Rose,Dresses,$84,item_4.png 5 | The Night Out,Olive Green,Dresses,$153,item_5.png 6 | Dawson Trolley,Pine Green,Shirts,$107,item_6.png 7 | Derby Hat,White,Hats,$100,item_1.png 8 | Long Sleever Tee,Baby Blue,Shirts,$62,item_8.png 9 | Simple Cardigan,Olive Green,Sweaters,$238,item_9.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | 8 | # Installer logs 9 | pip-log.txt 10 | pip-delete-this-directory.txt 11 | 12 | # Unit test / coverage reports 13 | htmlcov/ 14 | .tox/ 15 | .coverage 16 | .coverage.* 17 | .cache 18 | .pytest_cache/ 19 | nosetests.xml 20 | coverage.xml 21 | *.cover 22 | .hypothesis/ 23 | 24 | # Environments 25 | .env 26 | .venv 27 | env/ 28 | venv*/ 29 | ENV/ 30 | env.bak/ 31 | venv.bak/ 32 | -------------------------------------------------------------------------------- /test_webhook.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import requests 4 | import json 5 | 6 | # Update project_id 7 | project_id = "" 8 | 9 | data = {"timestamp": 1463602412, "project_id": project_id, "data": {"cdn_url": "https://cdn.optimizely.com/json/{0}.json".format(project_id), "origin_url": "https://optimizely.s3.amazonaws.com/json/{0}.json".format(project_id), "revision": 15}, "event": "project.datafile_updated"} 10 | 11 | r = requests.post("http://127.0.0.1:4001/webhook", data=json.dumps(data), headers={'Content-Type': 'application/json'}) 12 | 13 | print(r.text) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Optimizely Inc 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /templates/purchase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Attic & Button 6 | 7 | 8 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |

This "purchase" event has been tracked! Thanks for checking out our demo app!

23 | Back to Catalog 24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Attic & Button 5 | 6 | 7 | 15 | 16 | 17 |
18 |
19 | 20 |

Welcome to Attic & Button!

21 |
22 |
23 | Simulate a visitor: 24 | 25 |
26 |
27 | {% if variation_key %} 28 |

Optimizely variation: {{variation_key}}

29 | {% endif %} 30 | {% if data %} 31 | 32 | {% for i in [0,1,2] %} 33 | 34 | {% for item in data[i*3:i*3+3] %} 35 | 46 | 47 |

48 | {% endfor %} 49 | 50 | 51 | {% endfor %} 52 |
36 |

{{item['name']}}

37 | in {{item['color']}}
38 | {{item['category']}}, ${{item['price']}} 39 | 40 |
41 | 42 | 43 | 44 |
45 |
53 | {% endif %} 54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | # Python SDK Demo App 2 | # Copyright 2016 Optimizely. Licensed under the Apache License 3 | # View the documentation: http://optimize.ly/py-sdk 4 | from __future__ import print_function 5 | 6 | import csv 7 | import json 8 | import os 9 | from operator import itemgetter 10 | 11 | 12 | #from optimizely_config_manager import OptimizelyConfigManager 13 | 14 | 15 | '''2020 updates: 16 | - updated the SDK version to the latest in requirements.txt 17 | - deleted optimizely_config_manager.py + its references (get_obj) 18 | - renamed project_ID to SDK_KEY, renamed config_manager to optimizely_client to better reflect the current SDK 19 | ''' 20 | from optimizely import optimizely 21 | from optimizely.config_manager import PollingConfigManager 22 | from optimizely import logger 23 | import logging 24 | 25 | #actually teh SDK_KEY 26 | SDK_KEY = 'PnsTgkYA2fJUhHZRnZ9S5f' 27 | 28 | 29 | CONF_MANAGER = PollingConfigManager( 30 | sdk_key = SDK_KEY, 31 | update_interval=10 32 | ) 33 | 34 | 35 | 36 | 37 | from flask import Flask, render_template, request 38 | 39 | 40 | 41 | 42 | application = Flask(__name__, static_folder='images') 43 | application.secret_key = os.urandom(24) 44 | 45 | 46 | optimizely_client = optimizely.Optimizely(config_manager=CONF_MANAGER, logger=logger.SimpleLogger(min_level=logging.INFO)) 47 | #optimizely_client = optimizely.Optimizely(datafile, logger=logger.SimpleLogger(min_level=logging.INFO)) 48 | 49 | 50 | 51 | def build_items(): 52 | items = [] 53 | reader = csv.reader(open('items.csv', 'r')) 54 | for line in reader: 55 | items.append({'name': line[0], 56 | 'color': line[1], 57 | 'category': line[2], 58 | 'price': int(line[3][1:]), 59 | 'imageUrl': line[4]}) 60 | return items 61 | 62 | # render homepage 63 | @application.route('/') 64 | def index(): 65 | items = build_items() 66 | return render_template('index.html', data=items) 67 | 68 | # display items 69 | @application.route('/shop', methods=['GET', 'POST']) 70 | def shop(): 71 | user_id = request.form['user_id'] 72 | 73 | # compute variation_key 74 | variation_key = optimizely_client.activate('ab_test_1', user_id) 75 | 76 | # load items 77 | sorted_items = build_items() 78 | 79 | # sort by price 80 | if variation_key == 'price': 81 | print("In variation 1") 82 | sorted_items = sorted(sorted_items, key=itemgetter('price')) 83 | 84 | # sort by category 85 | if variation_key == 'category': 86 | print("In variation 2") 87 | sorted_items = sorted(sorted_items, key=itemgetter('category')) 88 | 89 | return render_template('index.html', 90 | data=sorted_items, user_id=user_id, variation_key=variation_key) 91 | 92 | # process a purchase event 93 | @application.route('/buy', methods=['GET', 'POST']) 94 | def buy(): 95 | user_id = request.form['user_id'] 96 | 97 | # track conversion event 98 | optimizely_client.track("click_buy", user_id) 99 | 100 | return render_template('purchase.html') 101 | 102 | # webhook logic 103 | @application.route('/webhook', methods=['POST']) 104 | def webhook_event(): 105 | data = request.get_json() 106 | 107 | event_type = data['event'] 108 | # use CDN URL from webhook payload 109 | if event_type == 'project.datafile_updated': 110 | url = data['data']['cdn_url'] 111 | optimizely_client.set_obj(url) 112 | return json.dumps({'success':True}), 200, {'ContentType':'application/json'} 113 | 114 | return json.dumps({'success':False}), 400, {'ContentType':'application/json'} 115 | 116 | if __name__ == '__main__': 117 | application.debug = True 118 | application.run(port=4001) 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python SDK Demo App 2 | 3 | This demo uses the Python SDK, a part of Optimizely's Full Stack solution. It will walk you through: 4 | 5 | 1. How to bucket users into experiment variations 6 | 2. How to track conversion events 7 | 3. How to view the experiment’s results 8 | 4. How to receive experiment updates via webhooks 9 | 10 | ## Optimizely Full Stack Overview 11 | 12 | Optimizely Full Stack allows developers to run experiments anywhere in their code! The Python SDK 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/). 13 | 14 | * View the [Python Getting Started Guide](http://developers.optimizely.com/server/getting-started/index.html?language=python) 15 | 16 | * View the reference [documentation](http://developers.optimizely.com/server/reference/index.html?language=python). 17 | 18 | * Latest [Python SDK](https://github.com/optimizely/python-sdk) 19 | 20 | ## Demo App 21 | 22 | This example app simulates an online retailer testing the effects of sorting products by price vs category. 23 | 24 | Using the instructions below, you can run the app locally and mimic bucketing website visitors by entering unique user IDs into the search bar. For example, the user ID “Matt” would simulate a unique visitor and select a specific variation for that unique visitor. The variation that is given to a specific unique visitor, such as Matt, will be deterministic. This means as long as the experiment conditions remain the same, Matt will always get the same variation. 25 | 26 | 27 | 28 | ### Deploying the App 29 | 1. Login or create an [Optimizely Account](https://app.optimizely.com/signin). 30 | 2. Setup a Python project via the Optimizely dashboard. [Instructions](http://developers.optimizely.com/server/getting-started/index.html?language=python) 31 | 3. Create and run a [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) 32 | 4. In `application.py`, update the `project_id`, `experiment_key`, variation keys, and `event_key` where appropriate in code. 33 | 5. Install requirements: `pip install -r requirements.txt` 34 | 6. Run the application `python application.py` (be sure the virtualenv is running) 35 | 7. You’re all set. Play around and view the experiment's results! 36 | 37 | To better understand this experiment, we recommend you bucket a few different visitors into variations and simulate a conversion event by clicking the Buy Now button. Within a few seconds, you should see the results populate on the Optimizely results page. 38 | 39 | Note: The webhook service should be used to determine when changes have been made to an experiment. A test script, `test_webhook.py`, has been provided to mimic a webhook event. If you run this file (`python test_webhook.py`) it will send a POST request with a sample webhook payload. Before running, update the `project_id` on line 4. 40 | 41 | ### Building the App 42 | 43 | 44 | First, we must initialize the Optimizely Python SDK. The `optimizely_config_manager.py` module handles setting the Optimizely client, as well as returning it. To instantiate an Optimizely client you must pass in a [datafile](http://developers.optimizely.com/server/reference/index.html#datafile). The datafile acts as a config file and represents the state of your Optimizely project. It contains information like the status of your experiments and variation traffic allocation. The `set_obj` function, retrieves the datafile and initializes an Optimizely object. 45 | 46 | ```python 47 | 48 | url = 'https://cdn.optimizely.com/json/{0}.json'.format(self.project_id) 49 | datafile = self.retrieve_datafile(url) 50 | self.obj = optimizely.Optimizely(datafile) 51 | ``` 52 | The crux of this Full Stack experiment is bucketing users into variations and exposing them to different sorting functions. The SDK’s `activate()` function will bucket users into a variation based on a user ID (internal id, user cookie, etc…). Below we use the variation key, returned by the SDK, to branch into a sorting algorithm based on price or category. 53 | 54 | ``` 55 | variation_key = config_manager.get_obj().activate(‘item-sort’', user_id) 56 | 57 | # sort by price 58 | if variation_key == 'price': 59 | sorted_items = sorted(sorted_items, key=itemgetter('price')) 60 | 61 | # sort by category 62 | if variation_key == 'category': 63 | sorted_items = sorted(sorted_items, key=itemgetter('category')) 64 | 65 | ``` 66 | To actually test which sorting algorithm influences increased sales, we need to track the number of clicks on the Buy Now button. We can leverage the `track()` function, which is subsequentlly called from the `buy()` function in `application.py`. To match the variation with the conversion event we must pass in the user_id. 67 | 68 | ``` 69 | # Track conversion event 70 | config_manager.track("item_purchase", user_id) 71 | ``` 72 | 73 | Lastly, let’s take a look at the `webhook_event()` endpoint. The webhook service alerts your application via an HTTP POST request when any changes are made to the project. You should provide your endpoint URL via the web app in Settings -> Webhooks. This is useful to ensure that your experiments are updated when you make changes in the web portal. The request will look like this: 74 | 75 | ``` 76 | "timestamp":1463602412, 77 | "project_id":1234, 78 | "data":{ 79 | "cdn_url":"https://cdn.optimizely.com/json/1234.json", 80 | "origin_url":"https://optimizely.s3.amazonaws.com/json/1234.json", 81 | "revision":15 82 | }, 83 | "event":"project.datafile_updated" 84 | ``` 85 | Our endpoint looks at the event type and then re-initializes the Optimizely object with this new Datafile --- again, this is handled by the `OptimizelyConfigManager` class. As mentioned above, you can use the `test_webhook.py` script to mimic webhook events. 86 | 87 | ## Getting Help! 88 | 89 | * Developer Docs: http://developers.optimizely.com/server 90 | * Questions? Shoot us an email at developers@optimizely.com 91 | --------------------------------------------------------------------------------