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:
Deeman
2026-02-19 20:07:50 +01:00
parent b108a53ef3
commit 5de83d820c
4 changed files with 30 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Simple A/B testing with `@ab_test` decorator and Umami `data-tag` integration
- SEO defaults in `base.html`: canonical, og:url, og:type, og:image (logo fallback), og:title, og:description, twitter:card — every page gets these automatically, child templates override as needed - SEO defaults in `base.html`: canonical, og:url, og:type, og:image (logo fallback), og:title, og:description, twitter:card — every page gets these automatically, child templates override as needed
- `robots.txt` route: disallows `/admin/`, `/auth/`, `/dashboard/`, `/billing/`, `/directory/results`; includes Sitemap reference - `robots.txt` route: disallows `/admin/`, `/auth/`, `/dashboard/`, `/billing/`, `/directory/results`; includes Sitemap reference
- Meta descriptions and OG tags on all public pages: about, terms, privacy, pricing, features, suppliers, directory, supplier detail - Meta descriptions and OG tags on all public pages: about, terms, privacy, pricing, features, suppliers, directory, supplier detail

View File

@@ -92,6 +92,8 @@ def create_app() -> Quart:
"is_admin": "admin" in (g.get("user") or {}).get("roles", []), "is_admin": "admin" in (g.get("user") or {}).get("roles", []),
"now": datetime.utcnow(), "now": datetime.utcnow(),
"csrf_token": get_csrf_token, "csrf_token": get_csrf_token,
"ab_variant": getattr(g, "ab_variant", None),
"ab_tag": getattr(g, "ab_tag", None),
} }
# Health check # Health check

View File

@@ -4,6 +4,7 @@ Core infrastructure: database, config, email, and shared utilities.
import hashlib import hashlib
import hmac import hmac
import os import os
import random
import re import re
import secrets import secrets
import unicodedata import unicodedata
@@ -15,7 +16,7 @@ from pathlib import Path
import aiosqlite import aiosqlite
import resend import resend
from dotenv import load_dotenv from dotenv import load_dotenv
from quart import g, request, session from quart import g, make_response, request, session
load_dotenv() 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"[^\w\s-]", "", text.lower())
text = re.sub(r"[-\s]+", "-", text).strip("-") text = re.sub(r"[-\s]+", "-", text).strip("-")
return text[:max_length_chars] 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

View File

@@ -20,7 +20,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
<!-- Umami Analytics --> <!-- 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 --> <!-- Paddle.js -->
<script defer src="https://cdn.paddle.com/paddle/v2/paddle.js"></script> <script defer src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>