Event Contract: pin.user-activity.session-ended (SessionEndedKafkaEvent)
Версионируемый контракт интеграционного события «Сессия завершена». Consumer-документы ссылаются сюда, не
переописывая payload. Событие — терминальное: ровно одно на сессию (INV-SES-006 InactiveImmutable). Дом
полей — entity: Session; здесь перечислено только то, что реально в payload.
Назначение
Фиксирует переход сессии Active → Inactive (закрытие). Публикуется в двух случаях: worker
activity-closer закрыл сессию по неактивности (порог 10–15 мин, конфиг) или связывание форс-закрыло
её при входе другого пользователя на устройстве (INV-SES-002). В момент закрытия сервер считает
Duration = LastActivityAt − CreatedAt (ADR-0056 п.3). Публичное: даёт downstream завершённую сессию с
готовой длительностью без синхронного запроса в user-activity — единственный обязательный consumer
dev-stats строит сессионные метрики (ADR-0057).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.user-activity.session-ended |
| Topic | pin.user-activity.session-ended.v1 (отдельный топик; session-linked — свой топик) |
| Partitions / key | 6 (1–10k активных сессий, ADR-0056) / key = SessionId (AggregateId) |
| ContractVersion | 1 |
| Producer | user-activity: worker activity-closer (закрытие по неактивности), связывание (форс-закрытие при входе другого пользователя, INV-SES-002) |
| Статус | draft |
Событие публикуется атомарно с мутацией агрегата. Метаданные события несут MessageId, CreatedAt,
Version (= ContractVersion), AggregateId = SessionId, AggregateType = "Session".
Payload SessionEndedKafkaEvent
Snapshot завершённой сессии. Дом полей — entity: Session (раздел «Поля»,
«Доменные и интеграционные события»).
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
SessionId | Guid | Да | Session.Id | Immutable | Ключ агрегата (= Kafka key). Идентификатор завершённой сессии. |
AnonymousId | Guid | Да | Session.AnonymousId | Immutable | Идентификатор устройства/браузера гостя; тот же id владеет гостевыми записями favorites. |
UserId | Guid? | Нет | Session.UserId | Immutable | Пользователь связанной сессии; null — сессия осталась гостевой. |
CreatedAt | DateTimeOffset | Да | Session.CreatedAt | Immutable | Начало сессии (первое действие устройства), время сервера. Нижняя граница Duration. |
LastActivityAt | DateTimeOffset | Да | Session.LastActivityAt | Immutable | Последняя активность (heartbeat/track), время сервера. Верхняя граница Duration. |
DurationSeconds | int | Да | Session.Duration (LastActivityAt − CreatedAt) | Immutable | Длительность сессии в секундах, ≥ 0 (chk_sessions_duration_nonneg). |
Вложенных моделей нет — payload плоский. Не входят в событие (правило session.md — «интеграционные
события»): TenantId (контекст вне tenant, ADR-0052 п.2); Device.* (UserAgent, квази-PII IpAddress);
EndedAt (диагностика лага polling-а, внутреннее); Status, LinkedAt.
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
activity-closer: now() − LastActivityAt > InactivityThreshold (10–15 мин, конфиг); выборка Active-сессий под блокировкой с пропуском занятых | Active → Inactive | закрытие агрегата + событие в одном согласованном изменении; в нём же Duration = LastActivityAt − CreatedAt, EndedAt = now(), закрытие открытых PropertyObjectView (INV-SES-005) |
Связывание: вход другого пользователя на устройстве с Active-сессией (UserId != actor, INV-SES-002) — форс-закрытие текущей сессии перед созданием новой | Active → Inactive | то же |
Событие терминальное: Inactive-сессия не изменяется никогда (INV-SES-006), поэтому на сессию приходится
не более одной публикации. Дубликаты возможны только на уровне доставки (at-least-once) — снимаются
идемпотентностью consumer-а по SessionId/MessageId.
Потребители (consumers)
| Consumer-контекст | Consumer | Side effects | Идемпотентность | Задержка (SLA) |
|---|---|---|---|---|
dev-stats | DevStatsSessionEndedConsumer | Сессионные метрики: длительность (DurationSeconds), число сессий, разрез гость/пользователь (по UserId != null) — ADR-0057 | first-seen по SessionId + MessageId | < 5 мин |
Других обязательных потребителей нет; топик открыт для будущих аналитических consumer-ов (доступ — m2m ACL Kafka).
Межтопиковый порядок. При форс-закрытии сессии (INV-SES-002) session-ended и
property-object-view-ended её закрытых просмотров публикуются в одном согласованном изменении, но в
разные топики — порядок доставки между ними не гарантирован. Consumer, читающий оба (dev-stats), не
должен полагаться на приход одного раньше другого: каждое событие самодостаточно и несёт собственную
длительность (DurationSeconds).
Версионирование и совместимость
ContractVersion = 1; при несовпадении версии consumer логирует warning и продолжает (upcasting/schema-migration нет). Версия в имени топика — по конвенции суффикс.v1.- Additive-only для minor: новые поля добавляются как optional с безопасным default — без bump.
- Breaking (удаление/переименование/смена типа или смысла) —
ContractVersion = 2+ новый топикpin.user-activity.session-ended.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Enum в payload нет (все поля скалярные); unknown-enum-обработка неприменима.
- Порядок доставки не значим: событие терминальное и единичное на
SessionId;Version-guard consumer-у не требуется (достаточно first-seen поSessionId).
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
SessionId, AnonymousId, UserId, CreatedAt, LastActivityAt, DurationSeconds | Internal | include |
Device.IpAddress | PII (квази) | exclude — не входит в событие; в логах producer маскируется до /24 |
Device.UserAgent | Internal | exclude — не входит в событие |
Actor/scope: событие публикует сам контекст (worker/связывание), не внешний актор; изоляция данных —
record-level по UserId/AnonymousId (вне tenant, ADR-0052 п.2). Событие НЕ содержит secrets, tokens, raw
credentials, email, полных PII/платёжных реквизитов, unbounded вложений.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: Session
- Use Cases (2): UC-ACT-001. Трекинг сессии гостя и пользователя, UC-ACT-005. Слияние гостевой и пользовательской активности при входе
- События (1): Event Contract: pin.user-activity.session-linked (SessionLinkedKafkaEvent)
- Консюмеры (2): Consumer: DevStatsSessionEndedConsumer, Consumer: Identity Sign-Ins (UserActivitySignedInConsumer)
- Фоновые задачи (1): Background Job: ActivityCloserJob
- registries (1): Реестр событий PIN
Связанные документы
- Сущность (дом полей и инварианты): session.md; соседний контракт топика-семейства: session-linked.md.
- Источники (producer): worker activity-closer, consumer identity-signed-in (связывание/форс-закрытие при входе).
- Границы контекста и карта событий: ../README.md.
- Решения: ADR-0052 (вне tenant), ADR-0054 (модуль
sessions), ADR-0056 п.3 (механизм закрытия иDuration), ADR-0057 (dev-stats— потребитель).