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

Event Contract: pin.user-activity.session-linked (SessionLinkedKafkaEvent)

Версионируемый контракт интеграционного события «сессия связана с пользователем». Consumer-документы ссылаются сюда, не переописывая payload.

Назначение

Фиксирует момент, когда гостевая сессия устройства связывается с учётной записью: Session.UserId переходит null → value при входе (INV-SES-002 «LinkOnce»). Публичное — даёт downstream-контекстам snapshot «этот AnonymousId теперь принадлежит этому UserId» без синхронного запроса в user-activity: CRM привязывает гостевую активность к лиду инвестора, favorites получает кандидата на merge гостевого избранного. Дом изменения — entity: Session (раздел «Доменные и интеграционные события», строка SessionLinkedKafkaEvent). Обязательных потребителей нет — событие включено на будущее (ADR-0054); триггер фактического merge избранного выбран фронтовый (README favorites).

Metadata

ПолеЗначение
Event typepin.user-activity.session-linked
Topicpin.user-activity.session-linked.v1
Partitions / key6 (ADR-0056 scale envelope) / key = AggregateId = SessionId (порядок per-session)
ContractVersion1
Produceruser-activity: линковка (POST /sessions/link, JWT claim sub) и UserActivitySignedInConsumer (событие pin.identity.user-signed-in)
Статусdraft

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

Payload SessionLinkedKafkaEvent

Дом полей — entity: Session; здесь перечислено только то, что реально в событии. Все поля скалярные — вложенных моделей нет. Payload не содержит Device.* (IpAddress/UserAgent — вне событий, правило session.md).

ПолеТипОбязательностьИсточник (поле агрегата)CompatibilityОписание
SessionIdGuidДаSession.IdImmutableКлюч агрегата (= Kafka key = AggregateId). Корень гостевой/пользовательской активности устройства.
AnonymousIdGuidДаSession.AnonymousIdImmutableУстройство/браузер, чья активность привязывается. Тот же id владеет гостевыми записями favorites (контракт X-Anonymous-Id).
UserIdGuidДаSession.UserIdImmutableПользователь, с которым связана сессия. В этом событии всегда заполнен (событие фиксирует переход null → value). Логическая ссылка identity.users(id).
LinkedAtDateTimeOffsetДаSession.LinkedAtImmutableМомент связывания (server clock, now()). Заполнен ⇔ UserId заполнен (chk_sessions_link_pair).

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

УсловиеПереход состоянияСогласованность
Вход на устройстве с гостевой Active-сессией: линковка (POST /sessions/link по JWT sub) либо UserActivitySignedInConsumer (событие pin.identity.user-signed-in) устанавливает UserId и LinkedAt (INV-SES-002)Session.UserId: null → value (LinkedAt = now()); Status остаётся Activeмутация агрегата + событие в одном согласованном изменении

Публикуется один раз на сессию (LinkOnce). Повторный вход того же пользователя — no-op (события нет). Вход другого пользователя на устройстве с уже связанной Active-сессией (INV-SES-002) не даёт session-linked: текущая сессия форс-закрывается (session-ended), а новая сессия рождается уже связанной (UserId установлен при создании — SessionStartedEvent, не переход null → value).

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

Обязательных consumer-ов нет (event включён на будущее, ADR-0054); ниже — кандидаты, сверено с README и session.md.

Consumer-контекстConsumerSide effectsИдемпотентностьЗадержка (SLA)
crm (кандидат)CrmSessionLinkedConsumerПривязка гостевой активности (AnonymousId) к лиду инвестора (UserId) — гостевые просмотры/действия «до входа» видны в карточке лидаMessageId + business key SessionId< 5 мин
favorites (кандидат)FavoritesSessionLinkedConsumerКандидат на merge гостевого избранного (AnonymousId → UserId); фактический триггер merge — фронтовый (README favorites), событие — резервный/аудит-путьMessageId (no-op при повторе)< 5 мин

Ретроактивная атрибуция (окно согласованности). Гостевые action-tracked/property-object-view-ended этого устройства (ключ/поле AnonymousId) могли прийти в crm/dev-stats до session-linked — топики независимы. Потребитель-кандидат crm привязывает уже полученную гостевую активность к лиду ретроактивно по AnonymousId → UserId из этого события (в пределах SLA < 5 мин); до обработки session-linked действия «до входа» в карточке лида ещё не сгруппированы под инвестором. Обработка идемпотентна по SessionId/MessageId — повторная доставка не меняет привязку.

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

  • ContractVersion = 1; при несовпадении версии consumer логирует warning и продолжает (upcasting/schema-migration нет). Топик кодирует версию суффиксом .v1 по конвенции.
  • Additive-only для minor: новые optional-поля с безопасным default — без bump.
  • Breaking (удаление/переименование/смена типа/смысла) — новый ContractVersion = 2 + топик pin.user-activity.session-linked.v2 + dual-publish на период миграции + deadline миграции consumers + sunset .v1.
  • Unknown enum — неприменимо (в payload enum-полей нет).
  • Порядок per-session гарантирован ключом SessionId; событие публикуется единожды на сессию, поэтому межсобытийного упорядочивания consumer-у держать не требуется (кроме дедупа по MessageId).

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

ПолеКласс данныхПравило
SessionId, AnonymousId, UserIdInternalinclude
LinkedAtInternalinclude

PII в payload нет — только идентификаторы и timestamp. Device.IpAddress (квази-PII), UserAgent и любые тексты пользователя в событие не включаются. Доступ к топику — m2m ACL Kafka (кандидаты crm/favorites). Событие НЕ содержит secrets, токенов, credentials, платёжных данных, unbounded-вложений.

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

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

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