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

Event Contract: pin.user-activity.property-object-view-ended (PropertyObjectViewEndedKafkaEvent)

Версионируемый контракт интеграционного события «Просмотр объекта недвижимости завершён». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей — entity: PropertyObjectView (раздел «Доменные и интеграционные события»); здесь перечислено только то, что реально в событии.

Назначение

Фиксирует закрытие одного просмотра карточки объекта недвижимости с измеренной длительностью. Producer — контекст user-activity: polling-worker activity-closer переводит PropertyObjectView Active → Inactive по порогу неактивности (delta now() − LastActivityAt) и атомарно с этим публикует событие (ADR-0056 п.3, INV-LV-004). Длительность просмотра — ключевая метрика (ADR-0056 п.2): событие делает её доступной downstream без синхронного запроса в user-activity. Публичное, потому что на нём строятся: собственные агрегаты популярности объекта недвижимости, застройщицкая аналитика (dev-stats) и лента карточки лида (crm).

Metadata

ПолеЗначение
Event typepin.user-activity.property-object-view-ended
Topicpin.user-activity.property-object-view-ended.v1
Partitions / key6 (ADR-0056: 1–10k активных) / key = PropertyObjectViewId (= AggregateId)
ContractVersion1
Produceruser-activity — worker activity-closer (закрытие просмотра по неактивности; форс-закрытие при закрытии/смене родительской сессии)
Статусdraft

Событие публикуется атомарно с закрытием агрегата. Метаданные события несут MessageId, CreatedAt, Version (= ContractVersion), AggregateId = PropertyObjectViewId (= Kafka key), AggregateType = "PropertyObjectView".

Payload PropertyObjectViewEndedKafkaEvent

Snapshot закрытого просмотра. Дом полей — PropertyObjectView; перечислено только то, что реально в событии. PII нет — только идентификаторы, длительность и снимок фильтра.

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
PropertyObjectViewIdGuidДаPropertyObjectView.IdImmutableКлюч агрегата (= Kafka key). Бизнес-ключ идемпотентности consumer-ов.
PropertyObjectIdGuidДаPropertyObjectView.PropertyObjectIdImmutableПросмотренный объект недвижимости (логическая ссылка chessboard/catalog, без FK).
SessionIdGuidДаPropertyObjectView.SessionIdImmutableСессия, в которой открыт объект недвижимости.
AnonymousIdGuidДаPropertyObjectView.AnonymousIdImmutableУстройство-владелец просмотра.
UserIdGuid?НетSession.UserId (актуальный на момент закрытия)ImmutableПользователь — актуальный из связанной сессии; null — гостевой просмотр (до входа).
ViewSourcestring (enum ViewSource)ДаPropertyObjectView.ViewSourceAdditive only (новые значения)Откуда открыта карточка: Catalog / Search / Selection / Favorites / News / DirectLink / Unknown.
ViewedAtDateTimeOffsetДаPropertyObjectView.CreatedAtImmutableМомент открытия карточки (начало просмотра).
LastActivityAtDateTimeOffsetДаPropertyObjectView.LastActivityAtImmutableПоследний heartbeat «карточка открыта» (конец измеренной активности; вход greatest для PropertyObjectViewStats.LastViewedAt).
DurationSecondsintДаPropertyObjectView.Duration (= LastActivityAt − CreatedAt, целые секунды)ImmutableДлительность просмотра. >= 0 (INV-LV: chk_property_object_views_duration_nonneg).
FilterSnapshotAppliedFilterModel?НетPropertyObjectView.FilterSnapshotAdditive onlyИммутабельный снимок фильтра, по которому найден объект недвижимости (null — просмотр без фильтра). Дом модели ниже.

Display-атрибуты объекта недвижимости (проект, застройщик, город, цена) в payload не включаются: consumers добирают их батчем из catalog по PropertyObjectId — синхронного вызова в catalog на каждый просмотр нет (INV-LV-006). В событии — только идентификаторы и снимок фильтра. Device.IpAddress/UserAgent родительской сессии (квази-PII) в событие не попадают (правило session.md).

Вложенная модель AppliedFilterModel (ContractVersion = 1)

Дом модели — action-event.md (раздел «AppliedFilterModel (jsonb, ContractVersion = 1)»); здесь — только дельта, без переописывания таблицы полей. Typed versioned jsonb: все поля optional (город/застройщик/проект/цена/комнаты/площадь/статусы/сортировка), минимум одно заполнено (INV-ACT-006); id-поля — логические ссылки на справочники chessboard/pin.handbook (мягкая валидация, см. дом модели), *Name-поля — денормализованные display-значения на момент применения (карточка лида читается без lookup-ов). Совместимость модели — additive-only в рамках ContractVersion = 1.

Когда публикуется

Единственный publisher — worker activity-closer (ADR-0056 п.3); публикация атомарна с закрытием записи.

УсловиеПереход состоянияСогласованность
now() − PropertyObjectView.LastActivityAt > InactivityThreshold (10–15 мин, конфиг); worker выбирает Active-просмотры под блокировкой с пропуском занятых (INV-LV-004)PropertyObjectView Active → Inactive: Status → Inactive, EndedAt = now(), Duration = LastActivityAt − CreatedAtзакрытие агрегата + событие в одном согласованном изменении
Закрытие/форс-закрытие родительской Session (INV-LV-007, INV-SES-002/005): все её Active-просмотры закрываются тем же согласованным изменениемActive → Inactive (та же вычислительная логика Duration)то же

Inactive-запись иммутабельна (INV-LV-008) — событие по одному PropertyObjectViewId публикуется ровно один раз; повторная доставка обрабатывается идемпотентностью consumer-ов.

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

Сверено с property-object-view.md («Доменные и интеграционные события») и property-object-view-stats.md.

Consumer-контекстConsumerSide effectsИдемпотентностьЗадержка (SLA)
user-activityPropertyObjectViewEndedConsumerИнкремент PropertyObjectViewStats по PropertyObjectId: ViewsCount += 1, TotalDuration += DurationSeconds, LastViewedAt = greatest(_, LastActivityAt)business key property-object-view-stats:{PropertyObjectViewId} (INV-LVS-003)< 1 мин
dev-statsDevStatsPropertyObjectViewEndedConsumerInsert факта просмотра в ClickHouse (многомерная аналитика застройщика: даты/зрители/иерархия проект→здание→планировка, ADR-0057)MessageId дедуп< 5 мин
crmCrmPropertyObjectViewEndedConsumerЭлемент PropertyObjectViewed в ленте карточки лида (привязка по UserId; длительность как сигнал интереса агенту)MessageId + business key PropertyObjectViewId< 5 мин

Гарантии доставки и порядок

  • Доставка — at-least-once, публикация атомарна с закрытием записи (outbox). Inactive-запись иммутабельна (INV-LV-008) ⇒ на один PropertyObjectViewId приходится ровно одна публикация; повтор возможен только на уровне доставки и снимается идемпотентностью consumer-ов (бизнес-ключ PropertyObjectViewId).
  • Порядок по ключу. Ключ PropertyObjectViewId — по одному событию на ключ, межсобытийного упорядочивания consumer держать не обязан. Агрегат PropertyObjectViewStats толерантен к порядку прихода (LastViewedAt = greatest(...), INV-LVS-004).
  • Межтопиковый порядок с session-ended не гарантирован (разные топики): при закрытии/форс-закрытии сессии её property-object-view-ended (по каждому просмотру) и session-ended публикуются в одном согласованном изменении, но идут независимо — consumer, читающий оба (dev-stats), не должен предполагать, что просмотры закрываются раньше или позже сессии; каждое событие самодостаточно и несёт собственную длительность.

Версионирование и совместимость

  • Версия — один ContractVersion = 1; при несовпадении версии consumer логирует warning и продолжает (upcasting/schema-migration нет). Топик кодирует версию суффиксом .v1 по конвенции.
  • Additive-only для minor: новые поля — optional с безопасным default, без bump (например, новые optional-поля AppliedFilterModel).
  • Breaking (удаление/переименование/смена типа или смысла) — новый ContractVersion = 2 + новый топик pin.user-activity.property-object-view-ended.v2 + параллельная публикация (dual-publish) на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum ViewSource — обрабатывается явно: сохранить строку как есть, warning, поведение как Unknown (fallback; producer уже нормализует недоверенный вход в Unknown — INV-LV upstream).

Безопасность

Actor producer — worker (system, без пользовательского контекста). Событие вне tenant (ADR-0052 п.2): изоляция record-level по UserId/AnonymousId — на стороне consumer-ов. PII в payload нет.

ПолеКласс данныхПравило
PropertyObjectViewId, PropertyObjectId, SessionId, AnonymousId, UserId, ViewedAt, LastActivityAt, DurationSecondsInternalinclude
ViewSourceInternalinclude
FilterSnapshot (AppliedFilterModel; *Name — денормализованные display-значения)Internalinclude; в логах не разворачивается (пишется только наличие/ContractVersion)

Событие НЕ содержит secrets, tokens, credentials, IpAddress/UserAgent, полные PII/платёжные реквизиты, приватные пользовательские данные, unbounded вложения.

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

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

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