feat(): added demo e2e command

This commit is contained in:
ITQ
2026-02-24 10:35:38 +03:00
parent 50941c1321
commit b7632ead43
3 changed files with 262 additions and 0 deletions
@@ -0,0 +1,262 @@
import json
import uuid
from decimal import Decimal
from typing import override
from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.decision.services import decide_for_flag
from apps.events.models import EventType
from apps.events.services import process_events_batch
from apps.experiments.services import (
experiment_approve,
experiment_create,
experiment_start,
experiment_submit_for_review,
variant_create,
)
from apps.flags.services import feature_flag_create
from apps.metrics.models import ExperimentMetric, MetricDefinition
from apps.reports.services import build_experiment_report
from apps.reviews.models import ApproverGroup
from apps.users.models import User, UserRole
class Command(BaseCommand):
help = (
"Run a complete E2E demo scenario: "
"flag -> experiment -> decide -> events -> report"
)
@override
def handle(self, *args, **options) -> None:
self._step("1. Create users")
owner = self._get_or_create_user(
"demo_experimenter", UserRole.EXPERIMENTER
)
approver = self._get_or_create_user("demo_approver", UserRole.APPROVER)
self._ensure_review_policy(owner, approver)
self._step("2. Create feature flag")
flag = feature_flag_create(
key=f"demo_button_color_{uuid.uuid4().hex[:6]}",
name="Demo Button Color",
value_type="string",
default_value="green",
)
self._ok(f"Flag created: key={flag.key}, default={flag.default_value}")
self._step("3. Create experiment with variants")
experiment = experiment_create(
flag=flag,
name="Button Color A/B Test",
owner=owner,
description="Testing blue vs red button colors",
hypothesis="Red button increases click-through rate",
traffic_allocation=Decimal("100.00"),
targeting_rules='country == "US"',
)
variant_create(
experiment=experiment,
user=owner,
name="control",
value="blue",
weight=Decimal("50.00"),
is_control=True,
)
variant_create(
experiment=experiment,
user=owner,
name="treatment",
value="red",
weight=Decimal("50.00"),
)
self._ok(
f"Experiment created: "
f"id={experiment.pk}, status={experiment.status}"
)
self._step("4. Lifecycle: draft → in_review → approved → running")
experiment = experiment_submit_for_review(
experiment=experiment, user=owner
)
self._ok(f"Status after submit: {experiment.status}")
experiment = experiment_approve(
experiment=experiment, approver=approver, comment="Looks good"
)
self._ok(f"Status after approve: {experiment.status}")
experiment = experiment_start(experiment=experiment, user=owner)
self._ok(f"Status after start: {experiment.status}")
self._step("5. Create event types")
self._get_or_create_event_type(
"exposure",
"Exposure",
is_exposure=True,
requires_exposure=False,
)
self._get_or_create_event_type(
"click",
"Click",
requires_exposure=True,
)
self._step("6. Create metrics and attach to experiment")
click_metric = self._get_or_create_metric(
"click_rate",
"Click Rate",
)
ExperimentMetric.objects.get_or_create(
experiment=experiment,
metric=click_metric,
defaults={"is_primary": True},
)
self._step("7. Make decisions for multiple subjects")
decisions = {}
subjects = [f"user_{i}" for i in range(1, 11)]
for subj in subjects:
result = decide_for_flag(flag.key, subj, {"country": "US"})
decisions[subj] = result
self._ok(
f" {subj}: value={result['value']}, "
f"reason={result['reason']}, "
f"variant_id={result.get('variant_id')}"
)
self._step("7b. Decide for non-targeted user")
result_outside = decide_for_flag(
flag.key, "user_de", {"country": "DE"}
)
self._ok(
f" user_de: value={result_outside['value']}, "
f"reason={result_outside['reason']} (expected: targeting_mismatch)"
)
self._step("8. Send exposure events")
now = timezone.now().isoformat()
exposure_events = []
for subj, dec in decisions.items():
if dec["reason"] != "experiment_assigned":
continue
exposure_events.append(
{
"event_id": str(uuid.uuid4()),
"event_type": "exposure",
"decision_id": dec["decision_id"],
"subject_id": subj,
"timestamp": now,
"properties": {},
}
)
batch_result = process_events_batch(exposure_events)
self._ok(
f"Exposures: accepted={batch_result.accepted}, "
f"duplicates={batch_result.duplicates}, "
f"rejected={batch_result.rejected}"
)
self._step("9. Send conversion (click) events for half the subjects")
assigned = [
s
for s, d in decisions.items()
if d["reason"] == "experiment_assigned"
]
click_events = [
{
"event_id": str(uuid.uuid4()),
"event_type": "click",
"decision_id": decisions[subj]["decision_id"],
"subject_id": subj,
"timestamp": now,
"properties": {"element": "buy_button"},
}
for subj in assigned[: len(assigned) // 2]
]
batch_result = process_events_batch(click_events)
self._ok(
f"Clicks: accepted={batch_result.accepted}, "
f"duplicates={batch_result.duplicates}, "
f"rejected={batch_result.rejected}"
)
self._step("10. Build experiment report")
report = build_experiment_report(experiment)
self.stdout.write(json.dumps(report, indent=2, default=str))
self._step("E2E demo completed successfully!")
def _step(self, msg: str) -> None:
self.stdout.write(self.style.MIGRATE_HEADING(f"\n=== {msg}"))
def _ok(self, msg: str) -> None:
self.stdout.write(self.style.SUCCESS(msg))
def _get_or_create_user(
self,
username: str,
role: str,
) -> User:
user, created = User.objects.get_or_create(
username=username,
defaults={
"email": f"{username}@lotty.local",
"role": role,
},
password="password123",
)
if created:
user.set_password("password123")
user.save()
status = "created" if created else "exists"
self._ok(
f"User {status}: {username} ({role})",
)
return user
def _ensure_review_policy(self, owner: User, approver: User) -> None:
group, _ = ApproverGroup.objects.get_or_create(
experimenter=owner,
defaults={"min_approvals": 1},
)
group.approvers.add(approver)
def _get_or_create_event_type(
self,
name: str,
display_name: str,
*,
is_exposure: bool = False,
requires_exposure: bool,
):
et, created = EventType.objects.get_or_create(
name=name,
defaults={
"display_name": display_name,
"is_exposure": is_exposure,
"requires_exposure": requires_exposure,
},
)
et_status = "created" if created else "exists"
self._ok(f"EventType {et_status}: {name}")
return et
def _get_or_create_metric(self, key: str, name: str) -> MetricDefinition:
metric, created = MetricDefinition.objects.get_or_create(
key=key,
defaults={
"name": name,
"metric_type": "ratio",
"direction": "higher_is_better",
"calculation_rule": {
"numerator_event": "click",
"denominator_event": "exposure",
},
},
)
m_status = "created" if created else "exists"
self._ok(f"Metric {m_status}: {key}")
return metric