Перейти к основному содержимому

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 typepin.user-activity.action-tracked
Topicpin.user-activity.action-tracked.v1
Partitions / key6 (объём трекинга, ADR-0056) / ключ = AnonymousId — порядок ленты действий per-device (кросс-девайсный порядок пользователя пересобирается по OccurredAt)
ContractVersion1
Produceruser-activity: модуль actions — track-API (track-action для ApplyFilter/Search), то же согласованное изменение, что создание PropertyObjectView (ViewPropertyObject), консюмеры favorites/selections (AddToFavorites/RemoveFromFavorites/AddToSelection)
Owning teamPIN Engagement
Статусdraft

Событие публикуется атомарно с вставкой записи ActionEvent. Метаданные события несут MessageId, CreatedAt, Version (= ContractVersion), AggregateId = ActionEventId, AggregateType = "ActionEvent".

Payload ActionTrackedKafkaEvent

Базовые поля зеркалят колонки TPH-записи ActionEvent (дом — action-event.md); подтипные данные вынесены в типизированный полиморфный Payload (см. ниже), а не в «голый» json.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
ActionEventIdGuidДаActionEvent.IdImmutableИдентификатор события действия (= AggregateId).
SessionIdGuidДаActionEvent.SessionIdImmutableСессия, в которой совершено действие.
UserIdGuid?НетActionEvent.UserId (снимок)Immutable semanticsПользователь на момент действия; null — гость.
AnonymousIdGuidДаActionEvent.AnonymousIdImmutableУстройство actor-а (= Kafka key).
ActionTypestring (enum ActionEventType)ДаActionEvent.ActionTypeAdditive only (новые значения)Дискриминатор: ApplyFilter / Search / ViewPropertyObject / AddToFavorites / RemoveFromFavorites / AddToSelection.
TargetIdGuid?НетActionEvent.PropertyObjectId (для ViewPropertyObject/*Favorites/AddToSelection)Additive onlyОсновной целевой объект действия (объект недвижимости); null для ApplyFilter/Search. Денормализован из Payload.PropertyObjectId — роутинг/фильтрация без десериализации полиморфного Payload.
PayloadActionPayloadДаподтип TPH (см. ниже)Additive onlyТипизированный полиморфный payload по ActionType.
OccurredAtDateTimeOffsetДаActionEvent.OccurredAtImmutableМомент действия (server clock only, INV-ACT-005).

Типизированный Payload (ActionPayload)

ActionPayload — абстрактная базовая модель без собственных полей; конкретный подтип выбирается по дискриминатору ActionType (полиморфная сериализация по дискриминатору, тот же набор значений, что TPH-дискриминатор). Поля подтипов — дельта TPH-колонок action-event.md; лишние для типа поля не передаются.

ApplyFilterActionPayload (ActionType = ApplyFilter)

ПолеТипОбязательностьОграниченияОписание
FilterAppliedFilterModelДаtyped jsonb, ContractVersion = 1; дом модели — action-event.md#appliedfiltermodel (ref-модели справочников: город — pin.handbook, CityId long; застройщик/проект — chessboard; мягкая валидация — см. дом модели)Снимок применённого фильтра (город/застройщик/проект/цена и т.д.).
ResultsCountint?Нет>= 0Размер выдачи по фильтру.

SearchActionPayload (ActionType = Search)

ПолеТипОбязательностьОграниченияОписание
SearchQuerystringДа1..512 символов после trimТекст поискового запроса (класс Internal — см. «Безопасность»).
ResultsCountint?Нет>= 0Размер выдачи по запросу.

ViewPropertyObjectActionPayload (ActionType = ViewPropertyObject)

ПолеТипОбязательностьОграниченияОписание
PropertyObjectIdGuidДалогическая ссылка на объект недвижимости chessboard/catalog (без FK)Открытый объект недвижимости.
PropertyObjectViewIdGuidДассылка на запись PropertyObjectViewПросмотр-источник (длительность и фильтр-контекст читаются оттуда/из property-object-view-ended, здесь не дублируются).

AddToFavoritesActionPayload (ActionType = AddToFavorites) / RemoveFromFavoritesActionPayload (ActionType = RemoveFromFavorites)

ПолеТипОбязательностьОграниченияОписание
PropertyObjectIdGuidДалогическая ссылка на объект недвижимостиОбъект недвижимости, добавленный/удалённый из избранного.
FavoriteIdGuidДалогическая ссылка на favorites.favorites(id) (без FK)Запись избранного-источник. Favorite.Note не передаётся (приватна).

AddToSelectionActionPayload (ActionType = AddToSelection)

ПолеТипОбязательностьОграниченияОписание
PropertyObjectIdGuidДалогическая ссылка на объект недвижимостиОбъект недвижимости, добавленный в подборку.
SelectionIdGuidДалогическая ссылка на подборку 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 (PropertyObjectViewStartedEventViewPropertyObjectActionEvent)[*] → создана записьто же согласованное изменение, что создание PropertyObjectView
Консюмер favorites/selections (AddToFavorites/RemoveFromFavorites/AddToSelection, дедуп по SourceMessageId INV-ACT-004)[*] → создана записьзапись ActionEvent + событие (идемпотентно по SourceMessageId)

Потребители (consumers)

Сверено с README контекста и service-boundaries.md (поток «ACT → CRM»).

Consumer-контекстConsumerSide effectsИдемпотентностьЗадержка (SLA)
crmCrmActionTrackedConsumerЛента карточки лида: ApplyFilter → FilterApplied, AddToFavorites → FavoriteAdded, Search/ViewPropertyObject/AddToSelection — событий ленты; фильтрация по ActionType, привязка к лиду по UserId/AnonymousIdMessageId + бизнес-ключ ActionEventId< 1 мин
dev-statsDevStatsActionTrackedConsumerClickHouse 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, OccurredAtInternalinclude
Payload.Filter (AppliedFilterModel), Payload.PropertyObjectId/PropertyObjectViewId/FavoriteId/SelectionId, ResultsCountInternalinclude (id и параметры фильтра; PII нет)
Payload.SearchQueryInternalinclude (доступен агенту в карточке лида как «что искал клиент»); в логах — первые 32 символа

Событие НЕ содержит Favorite.Note, Device.IpAddress/UserAgent, secrets, tokens, полные PII или платёжные реквизиты — только id, typed-параметры фильтра/поиска и доменное время.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы