Repository: hedyorg/hedy

Issue:
A/B testing support
- Set up environment.
- Set up deterministic redirect in code.
- Check if we can make HTTPS and account logic work in this way.


Discussion:

Resolving patch:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -22,9 +22,10 @@
 from config import config
 from auth import auth_templates, current_user, requires_login, is_admin, is_teacher
 from utils import db_get, db_get_many, db_set, timems, type_check, object_check, db_del
+import hashlib
 
 # app.py
-from flask import Flask, request, jsonify, render_template, session, abort, g, redirect
+from flask import Flask, request, jsonify, render_template, session, abort, g, redirect, make_response
 from flask_compress import Compress
 
 # Hedy-specific modules
@@ -70,6 +71,58 @@
 
 app = Flask(__name__, static_url_path='')
 
+def hash_user_or_session (string):
+    hash = hashlib.md5 (string.encode ('utf-8')).hexdigest ()
+    return int (hash, 16)
+
+def redirect_ab (request, session):
+    # If the user is logged in, we use their username as identifier, otherwise we use the session id
+    user_identifier = current_user(request) ['username'] or str (session_id ())
+
+    # This will send 50% of the requests into redirect.
+    redirect_proportion = 50
+    redirect_flag = (hash_user_or_session (user_identifier) % 100) < redirect_proportion
+    return redirect_flag
+
+# If present, PROXY_TO_TEST_ENV should be the name of the target environment
+if os.getenv ('PROXY_TO_TEST_ENV') and not os.getenv ('IS_TEST_ENV'):
+    @app.before_request
+    def before_request():
+        # If it is an auth route, we do not reverse proxy it to the PROXY_TO_TEST_ENV environment, with the exception of /auth/texts
+        # We want to keep all cookie setting in the main environment, not the test one.
+        if (re.match ('.*/auth/.*', request.url) and not re.match ('.*/auth/texts', request.url)):
+            pass
+        # If we enter this block, we will reverse proxy the request to the PROXY_TO_TEST_ENV environment.
+        elif (redirect_ab (request, session)):
+
+            print ('DEBUG TEST - REVERSE PROXYING REQUEST', request.method, request.url, session_id ())
+
+            url = request.url.replace (os.getenv ('HEROKU_APP_NAME'), os.getenv ('PROXY_TO_TEST_ENV'))
+
+            request_headers = {}
+            for header in request.headers:
+                if (header [0].lower () in ['host']):
+                    continue
+                request_headers [header [0]] = header [1]
+
+            # Send the session_id to the test environment for logging purposes
+            request_headers ['x-session_id'] = session_id ()
+            r = getattr (requests, request.method.lower ()) (url, headers=request_headers, data=request.data)
+
+            response = make_response (r.content)
+            for header in r.headers:
+                # With great help from https://medium.com/customorchestrator/simple-reverse-proxy-server-using-flask-936087ce0afb
+                # We ignore the set-cookie header
+                if (header.lower () in ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'set-cookie']):
+                    continue
+                response.headers [header] = r.headers [header]
+            return response, r.status_code
+
+if os.getenv ('IS_TEST_ENV'):
+    @app.before_request
+    def before_request():
+        print ('DEBUG TEST - RECEIVE PROXIED REQUEST', request.method, request.url, session_id ())
+
 # HTTP -> HTTPS redirect
 # https://stackoverflow.com/questions/32237379/python-flask-redirect-to-https-from-http/32238093
 if os.getenv ('REDIRECT_HTTP_TO_HTTPS'):
@@ -143,7 +196,7 @@ def parse():
             print(f"error transpiling {code}")
             response["Error"] = str(E)
 
-    logger.log({
+    logger.log ({
         'session': session_id(),
         'date': str(datetime.datetime.now()),
         'level': level,
@@ -151,7 +204,8 @@ def parse():
         'code': code,
         'server_error': response.get('Error'),
         'version': version(),
-        'username': username
+        'username': username,
+        'is_test': 1 if os.getenv ('IS_TEST_ENV') else None
     })
 
     return jsonify(response)
@@ -160,14 +214,15 @@ def parse():
 def report_error():
     post_body = request.json
 
-    logger.log({
+    logger.log ({
         'session': session_id(),
         'date': str(datetime.datetime.now()),
         'level': post_body.get('level'),
         'code': post_body.get('code'),
         'client_error': post_body.get('client_error'),
         'version': version(),
-        'username': current_user(request) ['username'] or None
+        'username': current_user(request) ['username'] or None,
+        'is_test': 1 if os.getenv ('IS_TEST_ENV') else None
     })
 
     return 'logged'
@@ -333,6 +388,8 @@ def main_page(page):
 
 def session_id():
     """Returns or sets the current session ID."""
+    if os.getenv('IS_TEST_ENV') and 'x-session_id' in request.headers:
+        return request.headers['x-session_id']
     if 'session_id' not in session:
         session['session_id'] = uuid.uuid4().hex
     return session['session_id']


Test patch: