add simple A/B testing with @ab_test decorator and Umami data-tag
Assigns visitors to experiment variants via cookie (30-day), sets g.ab_variant and g.ab_tag, and conditionally adds data-tag to the Umami script tag so all pageviews and events are automatically tagged in the Umami dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,8 @@ def create_app() -> Quart:
|
||||
"is_admin": "admin" in (g.get("user") or {}).get("roles", []),
|
||||
"now": datetime.utcnow(),
|
||||
"csrf_token": get_csrf_token,
|
||||
"ab_variant": getattr(g, "ab_variant", None),
|
||||
"ab_tag": getattr(g, "ab_tag", None),
|
||||
}
|
||||
|
||||
# Health check
|
||||
|
||||
@@ -4,6 +4,7 @@ Core infrastructure: database, config, email, and shared utilities.
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
import unicodedata
|
||||
@@ -15,7 +16,7 @@ from pathlib import Path
|
||||
import aiosqlite
|
||||
import resend
|
||||
from dotenv import load_dotenv
|
||||
from quart import g, request, session
|
||||
from quart import g, make_response, request, session
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -390,3 +391,27 @@ def slugify(text: str, max_length_chars: int = 80) -> str:
|
||||
text = re.sub(r"[^\w\s-]", "", text.lower())
|
||||
text = re.sub(r"[-\s]+", "-", text).strip("-")
|
||||
return text[:max_length_chars]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# A/B Testing
|
||||
# =============================================================================
|
||||
|
||||
def ab_test(experiment: str, variants: tuple = ("control", "treatment")):
|
||||
"""Assign visitor to an A/B test variant via cookie, tag Umami pageviews."""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
async def wrapper(*args, **kwargs):
|
||||
cookie_key = f"ab_{experiment}"
|
||||
assigned = request.cookies.get(cookie_key)
|
||||
if assigned not in variants:
|
||||
assigned = random.choice(variants)
|
||||
|
||||
g.ab_variant = assigned
|
||||
g.ab_tag = f"{experiment}-{assigned}"
|
||||
|
||||
response = await make_response(await f(*args, **kwargs))
|
||||
response.set_cookie(cookie_key, assigned, max_age=30 * 24 * 60 * 60)
|
||||
return response
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
|
||||
|
||||
<!-- Umami Analytics -->
|
||||
<script defer src="https://umami.padelnomics.io/Z.js" data-website-id="4474414b-58d6-4c6e-89a1-df5ea1f49d70"></script>
|
||||
<script defer src="https://umami.padelnomics.io/Z.js" data-website-id="4474414b-58d6-4c6e-89a1-df5ea1f49d70"{% if ab_tag %} data-tag="{{ ab_tag }}"{% endif %}></script>
|
||||
|
||||
<!-- Paddle.js -->
|
||||
<script defer src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user