Event Contract: pin.user-activity.action-tracked (ActionTrackedKafkaEvent)
Версионируемый контракт интеграционного события «Действие пользователя зафиксировано». Consumer-документы
ссылаются сюда, не переописывая payload. Один топик с дискриминатором ActionType покрывает всю TPH-ленту
действий (применён фильтр, поиск, открыт объект недвижимости, избранное, подборка) — потребителю нужна вся
лента лида разом, фильтрация по типу на его стороне. Дом полей — entity: ActionEvent;
здесь перечислена только проекция, реально уходящая в событие.
Назначение
Фиксирует создание любой append-only записи ActionEvent (значимое действие
гостя/пользователя: применён фильтр каталога, текстовый поиск, открыта карточка объекта недвижимости, добавлено/удалено
избранное, добавлено в подборку). Публичное: даёт downstream-контекстам typed-снимок действия без
синхронного запроса в user-activity — лента карточки лида в crm (события FilterApplied/FavoriteAdded
и т.д.) и facts просмотров/воронки в dev-stats. Событие не содержит приватных данных (Favorite.Note,
IP/User-Agent устройства) — см. «Безопасность».
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.user-activity.action-tracked |
| Topic | pin.user-activity.action-tracked.v1 |
| Partitions / key | 6 (объём трекинга, ADR-0056) / ключ = AnonymousId — порядок ленты действий per-device (кросс-девайсный порядок пользователя пересобирается по OccurredAt) |
| ContractVersion | 1 |
| Producer | user-activity: модуль actions — track-API (track-action для ApplyFilter/Search), то же согласованное изменение, что создание PropertyObjectView (ViewPropertyObject), консюмеры favorites/selections (AddToFavorites/RemoveFromFavorites/AddToSelection) |
| Owning team | PIN Engagement |
| Статус | draft |
Событие публикуется атомарно с вставкой записи ActionEvent. Метаданные события несут MessageId,
CreatedAt, Version (= ContractVersion), AggregateId = ActionEventId, AggregateType = "ActionEvent".
Payload ActionTrackedKafkaEvent
Базовые поля зеркалят колонки TPH-записи ActionEvent (дом — action-event.md); подтипные данные вынесены в
типизированный полиморфный Payload (см. ниже), а не в «голый» json.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
ActionEventId | Guid | Да | ActionEvent.Id | Immutable | Идентификатор события действия (= AggregateId). |
SessionId | Guid | Да | ActionEvent.SessionId | Immutable | Сессия, в которой совершено действие. |
UserId | Guid? | Нет | ActionEvent.UserId (снимок) | Immutable semantics | Пользователь на момент действия; null — гость. |
AnonymousId | Guid | Да | ActionEvent.AnonymousId | Immutable | Устройство actor-а (= Kafka key). |
ActionType | string (enum ActionEventType) | Да | ActionEvent.ActionType | Additive only (новые значения) | Дискриминатор: ApplyFilter / Search / ViewPropertyObject / AddToFavorites / RemoveFromFavorites / AddToSelection. |
TargetId | Guid? | Нет | ActionEvent.PropertyObjectId (для ViewPropertyObject/*Favorites/AddToSelection) | Additive only | Основной целевой объект действия (объект недвижимости); null для ApplyFilter/Search. Денормализован из Payload.PropertyObjectId — роутинг/фильтрация без десериализации полиморфного Payload. |
Payload | ActionPayload | Да | подтип TPH (см. ниже) | Additive only | Типизированный полиморфный payload по ActionType. |
OccurredAt | DateTimeOffset | Да | ActionEvent.OccurredAt | Immutable | Момент действия (server clock only, INV-ACT-005). |
Типизированный Payload (ActionPayload)
ActionPayload — абстрактная базовая модель без собственных полей; конкретный подтип выбирается по
дискриминатору ActionType (полиморфная сериализация по дискриминатору, тот же набор значений, что
TPH-дискриминатор). Поля подтипов — дельта TPH-колонок action-event.md; лишние
для типа поля не передаются.
ApplyFilterActionPayload (ActionType = ApplyFilter)
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
Filter | AppliedFilterModel | Да | typed jsonb, ContractVersion = 1; дом модели — action-event.md#appliedfiltermodel (ref-модели справочников: город — pin.handbook, CityId long; застройщик/проект — chessboard; мягкая валидация — см. дом модели) | Снимок применённого фильтра (город/застройщик/проект/цена и т.д.). |
ResultsCount | int? | Нет | >= 0 | Размер выдачи по фильтру. |
SearchActionPayload (ActionType = Search)
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
SearchQuery | string | Да | 1..512 символов после trim | Текст поискового запроса (класс Internal — см. «Безопасность»). |
ResultsCount | int? | Нет | >= 0 | Размер выдачи по запросу. |
ViewPropertyObjectActionPayload (ActionType = ViewPropertyObject)
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
PropertyObjectId | Guid | Да | логическая ссылка на объект недвижимости chessboard/catalog (без FK) | Открытый объект недвижимости. |
PropertyObjectViewId | Guid | Да | ссылка на запись PropertyObjectView | Просмотр-источник (длительность и фильтр-контекст читаются оттуда/из property-object-view-ended, здесь не дублируются). |
AddToFavoritesActionPayload (ActionType = AddToFavorites) / RemoveFromFavoritesActionPayload (ActionType = RemoveFromFavorites)
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
PropertyObjectId | Guid | Да | логическая ссылка на объект недвижимости | Объект недвижимости, добавленный/удалённый из избранного. |
FavoriteId | Guid | Да | логическая ссылка на favorites.favorites(id) (без FK) | Запись избранного-источник. Favorite.Note не передаётся (приватна). |
AddToSelectionActionPayload (ActionType = AddToSelection)
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|---|---|---|---|
PropertyObjectId | Guid | Да | логическая ссылка на объект недвижимости | Объект недвижимости, добавленный в подборку. |
SelectionId | Guid | Да | логическая ссылка на подборку selections (без FK) | Подборка-приёмник. |
Когда публикуется
В отличие от session-ended/property-object-view-ended (публикуются worker-ом при закрытии), action-tracked
публикуется сразу — в том же согласованном изменении, что вставка append-only записи ActionEvent
(записи immutable, «закрытия» у действия нет, INV-ACT-001). Источник вставки зависит от ActionType:
| Условие | Переход состояния | Согласованность |
|---|---|---|
track-API track-action (ApplyFilter — фильтр не пуст INV-ACT-006; Search) | [*] → создана запись ActionEvent | запись ActionEvent + событие в одном согласованном изменении |
Создание PropertyObjectView (PropertyObjectViewStartedEvent → ViewPropertyObjectActionEvent) | [*] → создана запись | то же согласованное изменение, что создание PropertyObjectView |
Консюмер favorites/selections (AddToFavorites/RemoveFromFavorites/AddToSelection, дедуп по SourceMessageId INV-ACT-004) | [*] → создана запись | запись ActionEvent + событие (идемпотентно по SourceMessageId) |
Потребители (consumers)
Сверено с README контекста и service-boundaries.md (поток «ACT → CRM»).
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
crm | CrmActionTrackedConsumer | Лента карточки лида: ApplyFilter → FilterApplied, AddToFavorites → FavoriteAdded, Search/ViewPropertyObject/AddToSelection — событий ленты; фильтрация по ActionType, привязка к лиду по UserId/AnonymousId | MessageId + бизнес-ключ ActionEventId | < 1 мин |
dev-stats | DevStatsActionTrackedConsumer | ClickHouse facts: ViewPropertyObject — источник просмотров (open-факт), ApplyFilter/Search — воронка каталога; фильтрация по ActionType (длительность просмотра берётся из отдельного property-object-view-ended, ADR-0057) | MessageId / ActionEventId | < 5 мин |
Будущий кандидат — news (вход рекомендаций, ADR-0054): при подключении добавляется без bump (топик
publish-subscribe). PII в payload нет — доступ к топику ограничен m2m ACL Kafka (crm/dev-stats/news).
Гарантии доставки и порядок
- Доставка — at-least-once. Событие публикуется транзакционным outbox атомарно с записью
ActionEvent— потеря невозможна, но возможен повтор. Consumers дедуплицируют поMessageId+ бизнес-ключуActionEventId; для consumer-происхождения исходной записи дополнительно действует дедуп поSourceMessageId(INV-ACT-004): одна записьActionEvent= одно исходное действие/сообщение. - Порядок — per-device. Ключ партиционирования
AnonymousIdдаёт устойчивый порядок ленты действий в пределах устройства (все действия устройства — в одной партиции). Кросс-девайсный порядок одного пользователя consumer восстанавливает поOccurredAt(server clock, INV-ACT-005), не по Kafka-offset. - Идемпотентность у потребителя. Повторная публикация того же
ActionEventIdне должна порождать новых элементов ленты лида/фактов — потребители обязаны быть идемпотентны по ключам выше.
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии consumer логирует warning и продолжает (upcasting/schema-migration нет). Топик кодирует версию суффиксом.v1по конвенции.- Additive-only для minor: новые optional-поля payload и новые optional-поля подтипов
ActionPayload(в т.ч. additive-поляAppliedFilterModelв рамках еёContractVersion = 1) — с безопасным default, без bump. - Breaking (удаление/переименование/смена типа или смысла, breaking-изменение
AppliedFilterModel) — новыйContractVersion = 2+ топикpin.user-activity.action-tracked.v2+ dual-publish + deadline миграции consumers + sunset.v1. - Unknown enum
ActionType— fallback: сохранить строку как есть, warning, телоPayloadне десериализовать в конкретный подтип (пропустить/сложить в generic-хранилище consumer-а); новый подтип действия — additive.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
ActionEventId, SessionId, UserId, AnonymousId, ActionType, TargetId, OccurredAt | Internal | include |
Payload.Filter (AppliedFilterModel), Payload.PropertyObjectId/PropertyObjectViewId/FavoriteId/SelectionId, ResultsCount | Internal | include (id и параметры фильтра; PII нет) |
Payload.SearchQuery | Internal | include (доступен агенту в карточке лида как «что искал клиент»); в логах — первые 32 символа |
Событие НЕ содержит Favorite.Note, Device.IpAddress/UserAgent, secrets, tokens, полные PII или
платёжные реквизиты — только id, typed-параметры фильтра/поиска и доменное время.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: ActionEvent (TPH-иерархия событий действий), Entity / Aggregate: PostReaction + проекция PostRankingSignals (Design)
- API (1): GET /api/internal/user-activity/users/{userId}/timeline — Хронология действий пользователя (карточка лида CRM)
- Use Cases (4): UC-ACT-002. Трекинг длительности и числа просмотров объекта недвижимости, UC-ACT-003. Инвестор просматривает свою историю просмотров и действий, UC-ACT-004. Агент/менеджер видит активность инвестора (для карточки лида), UC-UAC-001. Агрегированная активность нескольких инвесторов-участников лида
- События (1): Event Contract: pin.user-activity.session-linked (SessionLinkedKafkaEvent)
- Консюмеры (1): Consumer: DevStatsActionTrackedConsumer
- registries (1): Реестр событий PIN
Связанные документы
- Сущность (дом полей и
AppliedFilterModel): action-event.md; связанные агрегаты: session.md, property-object-view.md; соседние контракты топиков:events/session-ended.md,events/property-object-view-ended.md. - Источники записей: track-API
../api/(track-action), консюмеры ../consumers/favorites-events.md, ../consumers/selections-events.md. - Обзор: ../README.md, service-boundaries.md (поток «ACT → CRM»).
- Решения: ADR-0052 (вне tenant), ADR-0054 (модуль
actions, typedActionEvent, входnews), ADR-0056 (simplicity-first), ADR-0057 (разграничение сdev-stats).