feat(generator): добавлен связанный визит в генератор

- Зачем:
  - нужен первый проверяемый срез новой модели, где один визит содержит несколько связанных событий.
- Что:
  - добавлен индекс browser-событий по click_id для выбора сид-визита как основы.
  - generate_batch теперь выпускает несколько browser-событий с одним новым click_id и общим device/geo-контекстом.
  - добавлен тест публичного контракта связанного визита.
- Проверка:
  - make generator-test.
This commit is contained in:
Dmitry Dementiev
2026-06-11 11:00:39 +03:00
parent f0370cdb90
commit d38124263a
2 changed files with 69 additions and 5 deletions
+32 -5
View File
@@ -153,12 +153,15 @@ class EventDictionary:
geo_events: list[dict[str, Any]]
# Индексы для быстрого поиска
browser_by_click_id: dict[str, list[dict]] = field(default_factory=dict)
location_by_event_id: dict[str, dict] = field(default_factory=dict)
device_by_click_id: dict[str, dict] = field(default_factory=dict)
geo_by_click_id: dict[str, dict] = field(default_factory=dict)
def __post_init__(self):
# Строим индексы для связности
for browser in self.browser_events:
self.browser_by_click_id.setdefault(browser["click_id"], []).append(browser)
for loc in self.location_events:
self.location_by_event_id[loc["event_id"]] = loc
for dev in self.device_events:
@@ -281,16 +284,40 @@ class EventGenerator:
"geo_events": [],
}
for _ in range(batch_size):
# Выбираем случайное браузерное событие как базу
if batch_size <= 0:
return batch
visit_candidates = [
click_id for click_id, browser_events in self.dictionary.browser_by_click_id.items()
if (
len(browser_events) >= batch_size
and click_id in self.dictionary.device_by_click_id
and click_id in self.dictionary.geo_by_click_id
and all(
event["event_id"] in self.dictionary.location_by_event_id
for event in browser_events[:batch_size]
)
)
]
if visit_candidates:
base_click_id = self.rng.choice(visit_candidates)
base_browser_events = self.dictionary.browser_by_click_id[base_click_id][:batch_size]
else:
# Крайний случай для очень малого сида: сохраняем форму визита,
# даже если приходится брать события с повторением.
base_browser = self.rng.choice(self.dictionary.browser_events)
base_click_id = base_browser["click_id"]
base_browser_events = [base_browser for _ in range(batch_size)]
base_device = self.dictionary.device_by_click_id.get(base_click_id)
base_geo = self.dictionary.geo_by_click_id.get(base_click_id)
new_click_id = self._new_uuid()
for base_browser in base_browser_events:
base_location = self.dictionary.location_by_event_id.get(base_browser["event_id"])
base_device = self.dictionary.device_by_click_id.get(base_browser["click_id"])
base_geo = self.dictionary.geo_by_click_id.get(base_browser["click_id"])
# Генерируем новые ID
new_event_id = self._new_uuid()
new_click_id = self._new_uuid()
new_timestamp = self._current_timestamp()
# Создаём новое браузерное событие
+37
View File
@@ -61,6 +61,43 @@ class TestEventGeneration:
assert len(batch["device_events"]) == 10
assert len(batch["geo_events"]) == 10
def test_generate_batch_creates_one_connected_visit(self, event_dictionary, base_config):
"""Публичный вызов генератора создаёт один связанный визит."""
generator = EventGenerator(event_dictionary, base_config)
batch = generator.generate_batch(3)
original_event_ids = {e["event_id"] for e in event_dictionary.browser_events}
original_click_ids = {e["click_id"] for e in event_dictionary.browser_events}
browser_events = batch["browser_events"]
location_events = batch["location_events"]
device_events = batch["device_events"]
geo_events = batch["geo_events"]
click_ids = {event["click_id"] for event in browser_events}
event_ids = [event["event_id"] for event in browser_events]
assert len(browser_events) > 1
assert len(click_ids) == 1
click_id = next(iter(click_ids))
assert click_id not in original_click_ids
uuid.UUID(click_id)
assert len(set(event_ids)) == len(event_ids)
assert all(event_id not in original_event_ids for event_id in event_ids)
for event_id in event_ids:
uuid.UUID(event_id)
assert {event["event_id"] for event in location_events} == set(event_ids)
assert {event["click_id"] for event in device_events} == {click_id}
assert {event["click_id"] for event in geo_events} == {click_id}
device_context = [{k: v for k, v in event.items() if k != "click_id"} for event in device_events]
geo_context = [{k: v for k, v in event.items() if k != "click_id"} for event in geo_events]
assert len({event["user_domain_id"] for event in device_events}) == 1
assert all(context == device_context[0] for context in device_context)
assert all(context == geo_context[0] for context in geo_context)
def test_event_ids_are_new_uuids(self, event_dictionary, base_config):
"""event_id и click_id — новые UUID, не из оригинальных данных."""
generator = EventGenerator(event_dictionary, base_config)