GET /api/internal/user-activity/users/{userId}/timeline — Хронология действий пользователя (карточка лида CRM)
Internal m2m-точка pull-чтения ленты действий пользователя для карточки лида CRM (B3,
поток «ACT → CRM» service-boundaries.md). Возвращает единую хронологию ActionEvent (просмотры объектов недвижимости,
применённые фильтры, поиск, избранное, подборки) с пагинацией и фильтром по дате/типу. Pull-двойник
push-события ActionTrackedKafkaEvent (pin.user-activity.action-tracked.v1):
CRM накапливает ленту консюмером, а этот endpoint даёт агенту дозагрузку/бэкфилл истории при открытии
карточки. Отличается от get-user-property-object-activity.md (только просмотры с
длительностями) — здесь вся лента действий; факт просмотра приходит записью ViewPropertyObjectActionEvent,
обогащённой длительностью из связанного PropertyObjectView.
Назначение
Отдать по идентификатору пользователя хронологический (новые сверху) список его действий на платформе PIN
для отображения в карточке лида CRM: что смотрел, какие фильтры/поиск применял, что добавлял в избранное и
подборки. Owning-контекст — user-activity (модуль actions).
| Поле | Значение |
|---|
| Сервис/контекст | user-activity |
| API area | internal (m2m: crm в рамках активного лида; Менеджер ПИН по permission) |
| Feature group | actions |
| Статус | draft |
| Документ | docs/06-services/user-activity/api/internal/get-user-action-timeline.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ServiceAccount (crm m2m — авторизацию «активный лид» делает CRM) или Operator(Admin) (Менеджер ПИН) |
| Auth policy / Permissions | m2m service-auth; permission user-activity.history.view-by-user — identity/domain/permission.md |
| Scope (RBAC) | платформа (внутренний потребитель); данные пользователя вне организации/проекта |
| Record-level | фильтр по целевому UserId из route + его SessionId (см. «Правило чтения», шаг 3); {userId} — доверенный параметр от internal-клиента |
| Tenant isolation | не применяется — вне tenant (ADR-0052 п.2); tenant-интерсепторы не подключаются, обязательный record-level фильтр по UserId/SessionId |
| Audit | read (лента — сам журнал действий, action-event.md); SearchQuery в логи не пишется полностью (первые 32 символа, Internal) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/internal/user-activity/users/{userId}/timeline |
| Success status | 200 (в т.ч. пустая страница, если у пользователя нет активности) |
| Idempotency header | не применяется (read-only, безопасный GET) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | route | Guid | Да | != Guid.Empty; логическая ссылка identity.users(id) — существование синхронно не проверяется (нет активности → пустая страница) | record-level target (доверенный internal-параметр) |
From | query | DateTimeOffset? | Нет | нижняя граница фильтра по серверному OccurredAt (>= From); From <= To, если оба заданы | read-фильтр над server-clock полем — не записывается (INV-ACT-005 не нарушается) |
To | query | DateTimeOffset? | Нет | верхняя граница (<= To) | read-фильтр |
ActionTypes | query (повторяемый) | IReadOnlyList<ActionEventType>? | Нет | подмножество enum ApplyFilter/Search/ViewPropertyObject/AddToFavorites/RemoveFromFavorites/AddToSelection; unknown-значения отбрасываются валидатором (unknown enum handling); пусто/null — все типы | — |
Paging | query (page,pageSize) | PagingQuery | Да | page >= 1; pageSize ∈ 1..100 (default 20) | защита от тяжёлых выборок |
Клиентские timestamp записей не принимаются: From/To — только границы чтения по серверному OccurredAt
(дом поля — action-event.md); сортировка и время события — серверные.
Модель ответа — страница UserActionTimelineItemModel
Ответ — страница ленты (Items, TotalCount, Page, PageSize). Элемент —
проекция-дельта ActionEvent (дом полей и семантика подтипов — action-event.md;
здесь только выдаваемые поля, зеркально payload-у action-tracked):
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
ActionEventId | Guid | Да | ActionEvent.Id | — | Идентификатор события действия (v7, сортируемый). |
ActionType | ActionEventType (enum-строка) | Да | ActionEvent.ActionType | — | Тип действия (TPH-дискриминатор). |
OccurredAt | DateTimeOffset | Да | ActionEvent.OccurredAt | — | Момент действия (server clock). |
SessionId | Guid | Да | ActionEvent.SessionId | — | Сессия действия (диагностика/группировка на стороне CRM). |
PropertyObjectId | Guid? | Нет | ActionEvent.PropertyObjectId | — | Объект недвижимости: ViewPropertyObject/*Favorites/AddToSelection. Display-данные объекта недвижимости CRM/фронт добирает в catalog. |
PropertyObjectViewId | Guid? | Нет | ViewPropertyObjectActionEvent.PropertyObjectViewId | — | Ссылка на запись просмотра (ViewPropertyObject). |
ViewDurationSeconds | int? | Нет | PropertyObjectView.Duration по PropertyObjectViewId (шаг 3) | — | Длительность просмотра; null — просмотр ещё Active (не закрыт worker-ом). |
FilterSnapshot | AppliedFilterModel? | Нет | ApplyFilterActionEvent.Filter | — | Typed-снимок фильтра (ApplyFilter); дом модели — action-event.md, ContractVersion = 1. |
ResultsCount | int? | Нет | ActionEvent.ResultsCount | — | Размер выдачи (ApplyFilter/Search). |
SearchQuery | string? | Нет | SearchActionEvent.SearchQuery | Internal (в ответ выдаётся — «что искал клиент»; в логах усечён) | Текст поиска (Search). |
FavoriteId | Guid? | Нет | ActionEvent.FavoriteId | — | Запись избранного (AddToFavorites/RemoveFromFavorites); Favorite.Note не выдаётся (приватна). |
SelectionId | Guid? | Нет | ActionEvent.SelectionId | — | Подборка (AddToSelection). |
Алгоритм
- Контекст и доступ. Вызывает
crm (m2m; авторизацию «активный лид» делает CRM) или Менеджер ПИН
(Operator); требуется permission user-activity.history.view-by-user. Запрос вне tenant (ADR-0052 п.2);
изоляция — обязательный record-level фильтр по UserId/SessionId.
- Проверка входа.
UserId != Guid.Empty; From <= To, если оба заданы; Page >= 1,
PageSize ∈ 1..100; неизвестные значения ActionTypes отбрасываются (unknown enum handling) — при иных
нарушениях VALIDATION_FAILED (400). UserId — логическая ссылка на identity, синхронно не
проверяется: неизвестный пользователь → пустая страница, не 404. У ActionEvent нет статуса
(append-only), допустимость состояния не проверяется.
- Что читаем.
- сессии пользователя (для истории «до и после входа», «Правило чтения» property-object-view.md):
их идентификаторы по
UserId;
- страница ленты
ActionEvent, где запись принадлежит пользователю напрямую
(UserId) или через его сессию (SessionId), с фильтром по OccurredAt (From/To) и по ActionType
(ActionTypes), отсортированная по OccurredAt (новые сверху), с пагинацией;
- обогащение длительностью: для элементов
ViewPropertyObject — Duration связанного
PropertyObjectView по PropertyObjectViewId страницы (закрытые —
значение, Active — null).
- Расчёт и маппинг. Переходов состояний нет (read-only). Маппинг поле-в-поле
ActionEvent (+ PropertyObjectView.Duration) → UserActionTimelineItemModel — по таблице «Модель ответа»;
FilterSnapshot читается как typed-снимок (ContractVersion = 1; при bump — обе версии).
- Результат. Страница ленты (
Items, TotalCount, Page, PageSize) → 200 (в т.ч. пустая для
неизвестного UserId). Консистентность eventual: недавно открытые просмотры без Duration (worker
activity-closer ещё не закрыл), consumer-записи (*Favorites/AddToSelection) появляются после
обработки их событий. Masking: SearchQuery выдаётся (Internal — по назначению карточки лида), в логах
усечён.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | none (read-only query) |
| External calls | none — UserId/PropertyObjectId логические ссылки, синхронно не резолвятся; display-данные объектов недвижимости добирает потребитель в catalog |
| Cache / projections / search | none — прямое btree-чтение PostgreSQL (ADR-0056: без Redis; GIN по filter_snapshot нет) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | UserId = Guid.Empty, From > To, PageSize вне 1..100 | «Проверьте параметры запроса» | no retry |
UNAUTHENTICATED | 401 | нет валидного m2m service-token | «Требуется аутентификация» | no retry |
USER_ACTIVITY_FORBIDDEN | 403 | нет permission user-activity.history.view-by-user | «Недостаточно прав» | no retry |
| — (не ошибка) | 200 | неизвестный UserId / нет действий | пустая PagingResult (TotalCount = 0) | — |
USER_ACTIVITY_UNAVAILABLE | 503 | PostgreSQL недоступен | «Сервис временно недоступен» | retry |
Acceptance-критерии
- Given пользователь с активностью, When
GET /users/{userId}/timeline, Then 200, страница
ленты (новые сверху) по записям пользователя напрямую (UserId) и через его сессии (SessionId).
- Given фильтр
ActionTypes, When GET, Then только указанные типы; неизвестные значения
ActionTypes молча отброшены (толерантная unknown-enum-обработка на входе).
- Given элемент
ViewPropertyObject по закрытому просмотру, When GET, Then ViewDurationSeconds
заполнен из связанного PropertyObjectView; по ещё Active-просмотру — null.
- Given consumer-записи (
AddToFavorites/AddToSelection) по недавним событиям favorites/selections,
When GET сразу после действия, Then они появляются после обработки их событий (freshness eventual).
- Given
From > To или pageSize вне 1..100, When GET, Then VALIDATION_FAILED (400).
- Given вызывающий без permission
user-activity.history.view-by-user, When GET, Then
USER_ACTIVITY_FORBIDDEN (403); без service-токена — 401.
- Given неизвестный
UserId, When GET, Then 200, пустая страница (TotalCount = 0), не 404.
- Given PostgreSQL недоступен, When
GET, Then USER_ACTIVITY_UNAVAILABLE (503), retry.
Совместимость и наблюдаемость
- Additive: новые подтипы
ActionEventType и optional-поля AppliedFilterModel — без breaking change;
потребитель обязан толерантно пропускать неизвестный ActionType (unknown enum handling), AppliedFilterModel
versioned (ContractVersion, чтение v1/v2). Удаление/переименование поля модели ответа — breaking
(согласование с crm + rollout).
- Logs:
timeline requested (UserId, TotalCount, набор ActionTypes, диапазон дат; SearchQuery
усечён до 32 символов, Internal); metrics: user_activity_timeline_requests_total{result}, latency p95
(внутренние списки p95 < 1 с, README «SLO»); traces — сквозной trace через traceparent; dashboard
pin-user-activity; runbook — docs/06-services/user-activity/runbooks/ (будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы