965af3a34c
F5: A/B Testing for Consent Rate - Migration 116: banner_variants table + variant tracking in audit log - BannerABService: deterministic sticky bucketing via device hash, chi-squared significance testing, variant CRUD - banner_ab_routes: 6 endpoints (CRUD + stats + assign) - ABTestPanel.tsx: variant creation, traffic sliders, opt-in comparison chart with winner/significance badges - New "A/B-Test" tab in cookie-banner page F8: Compliance Report PDF - CompliancePDFGenerator: reportlab-based A4 PDF covering all modules (Company Profile, TOM, VVT, DSFA, Risks, Vendors, Incidents, Reviews, Consents, Roles) - compliance_report_routes: GET /compliance/report/pdf - "Compliance-Report herunterladen" button on SDK dashboard [migration-approved] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
121 lines
3.6 KiB
Python
121 lines
3.6 KiB
Python
"""
|
|
FastAPI routes for Banner A/B Testing.
|
|
|
|
Endpoints:
|
|
GET /banner/ab/{site_config_id}/variants — list variants
|
|
POST /banner/ab/{site_config_id}/variants — create variant
|
|
PUT /banner/ab/variants/{variant_id} — update variant
|
|
DELETE /banner/ab/variants/{variant_id} — delete variant
|
|
GET /banner/ab/{site_config_id}/stats — per-variant stats
|
|
GET /banner/ab/assign — assign variant for device
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from classroom_engine.database import get_db
|
|
from .tenant_utils import get_tenant_id as _get_tenant_id
|
|
from compliance.services.banner_ab_service import BannerABService
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/banner/ab", tags=["banner-ab-testing"])
|
|
|
|
|
|
class VariantCreate(BaseModel):
|
|
variant_name: str
|
|
variant_key: str = "A"
|
|
traffic_percent: int = 50
|
|
is_control: bool = False
|
|
banner_title: Optional[str] = None
|
|
banner_description: Optional[str] = None
|
|
position: Optional[str] = None
|
|
style: Optional[str] = None
|
|
primary_color: Optional[str] = None
|
|
show_decline_all: Optional[bool] = None
|
|
theme_overrides: Optional[dict] = None
|
|
|
|
|
|
class VariantUpdate(BaseModel):
|
|
variant_name: Optional[str] = None
|
|
traffic_percent: Optional[int] = None
|
|
is_control: Optional[bool] = None
|
|
banner_title: Optional[str] = None
|
|
banner_description: Optional[str] = None
|
|
position: Optional[str] = None
|
|
style: Optional[str] = None
|
|
primary_color: Optional[str] = None
|
|
show_decline_all: Optional[bool] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
@router.get("/{site_config_id}/variants")
|
|
def list_variants(
|
|
site_config_id: str,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
service = BannerABService(db)
|
|
return service.list_variants(tenant_id, site_config_id)
|
|
|
|
|
|
@router.post("/{site_config_id}/variants")
|
|
def create_variant(
|
|
site_config_id: str,
|
|
body: VariantCreate,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
service = BannerABService(db)
|
|
return service.create_variant(tenant_id, site_config_id, body.model_dump())
|
|
|
|
|
|
@router.put("/variants/{variant_id}")
|
|
def update_variant(
|
|
variant_id: str,
|
|
body: VariantUpdate,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
service = BannerABService(db)
|
|
result = service.update_variant(variant_id, body.model_dump(exclude_none=True))
|
|
if not result:
|
|
raise HTTPException(404, "Variant not found")
|
|
return result
|
|
|
|
|
|
@router.delete("/variants/{variant_id}")
|
|
def delete_variant(
|
|
variant_id: str,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
service = BannerABService(db)
|
|
if not service.delete_variant(variant_id):
|
|
raise HTTPException(404, "Variant not found")
|
|
return {"deleted": True}
|
|
|
|
|
|
@router.get("/{site_config_id}/stats")
|
|
def variant_stats(
|
|
site_config_id: str,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
service = BannerABService(db)
|
|
return service.get_variant_stats(tenant_id, site_config_id)
|
|
|
|
|
|
@router.get("/assign")
|
|
def assign_variant(
|
|
site_config_id: str = Query(...),
|
|
device_fingerprint: str = Query(...),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
service = BannerABService(db)
|
|
variant = service.assign_variant(site_config_id, device_fingerprint)
|
|
if not variant:
|
|
return {"variant": None, "message": "No active A/B test"}
|
|
return {"variant": variant}
|