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

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).

Metadata

ПолеЗначение
Сервис/контекстuser-activity
API areainternal (m2m: crm в рамках активного лида; Менеджер ПИН по permission)
Feature groupactions
Статусdraft
Документdocs/06-services/user-activity/api/internal/get-user-action-timeline.md

Актор и доступ

ПроверкаЗначение
Actor typeServiceAccount (crm m2m — авторизацию «активный лид» делает CRM) или Operator(Admin) (Менеджер ПИН)
Auth policy / Permissionsm2m service-auth; permission user-activity.history.view-by-useridentity/domain/permission.md
Scope (RBAC)платформа (внутренний потребитель); данные пользователя вне организации/проекта
Record-levelфильтр по целевому UserId из route + его SessionId (см. «Правило чтения», шаг 3); {userId} — доверенный параметр от internal-клиента
Tenant isolationне применяется — вне tenant (ADR-0052 п.2); tenant-интерсепторы не подключаются, обязательный record-level фильтр по UserId/SessionId
Auditread (лента — сам журнал действий, action-event.md); SearchQuery в логи не пишется полностью (первые 32 символа, Internal)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/internal/user-activity/users/{userId}/timeline
Success status200 (в т.ч. пустая страница, если у пользователя нет активности)
Idempotency headerне применяется (read-only, безопасный GET)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdrouteGuidДа!= Guid.Empty; логическая ссылка identity.users(id) — существование синхронно не проверяется (нет активности → пустая страница)record-level target (доверенный internal-параметр)
FromqueryDateTimeOffset?Нетнижняя граница фильтра по серверному OccurredAt (>= From); From <= To, если оба заданыread-фильтр над server-clock полем — не записывается (INV-ACT-005 не нарушается)
ToqueryDateTimeOffset?Нетверхняя граница (<= To)read-фильтр
ActionTypesquery (повторяемый)IReadOnlyList<ActionEventType>?Нетподмножество enum ApplyFilter/Search/ViewPropertyObject/AddToFavorites/RemoveFromFavorites/AddToSelection; unknown-значения отбрасываются валидатором (unknown enum handling); пусто/null — все типы
Pagingquery (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Описание
ActionEventIdGuidДаActionEvent.IdИдентификатор события действия (v7, сортируемый).
ActionTypeActionEventType (enum-строка)ДаActionEvent.ActionTypeТип действия (TPH-дискриминатор).
OccurredAtDateTimeOffsetДаActionEvent.OccurredAtМомент действия (server clock).
SessionIdGuidДаActionEvent.SessionIdСессия действия (диагностика/группировка на стороне CRM).
PropertyObjectIdGuid?НетActionEvent.PropertyObjectIdОбъект недвижимости: ViewPropertyObject/*Favorites/AddToSelection. Display-данные объекта недвижимости CRM/фронт добирает в catalog.
PropertyObjectViewIdGuid?НетViewPropertyObjectActionEvent.PropertyObjectViewIdСсылка на запись просмотра (ViewPropertyObject).
ViewDurationSecondsint?НетPropertyObjectView.Duration по PropertyObjectViewId (шаг 3)Длительность просмотра; null — просмотр ещё Active (не закрыт worker-ом).
FilterSnapshotAppliedFilterModel?НетApplyFilterActionEvent.FilterTyped-снимок фильтра (ApplyFilter); дом модели — action-event.md, ContractVersion = 1.
ResultsCountint?НетActionEvent.ResultsCountРазмер выдачи (ApplyFilter/Search).
SearchQuerystring?НетSearchActionEvent.SearchQueryInternal (в ответ выдаётся — «что искал клиент»; в логах усечён)Текст поиска (Search).
FavoriteIdGuid?НетActionEvent.FavoriteIdЗапись избранного (AddToFavorites/RemoveFromFavorites); Favorite.Note не выдаётся (приватна).
SelectionIdGuid?НетActionEvent.SelectionIdПодборка (AddToSelection).

Алгоритм

  1. Контекст и доступ. Вызывает crm (m2m; авторизацию «активный лид» делает CRM) или Менеджер ПИН (Operator); требуется permission user-activity.history.view-by-user. Запрос вне tenant (ADR-0052 п.2); изоляция — обязательный record-level фильтр по UserId/SessionId.
  2. Проверка входа. UserId != Guid.Empty; From <= To, если оба заданы; Page >= 1, PageSize ∈ 1..100; неизвестные значения ActionTypes отбрасываются (unknown enum handling) — при иных нарушениях VALIDATION_FAILED (400). UserId — логическая ссылка на identity, синхронно не проверяется: неизвестный пользователь → пустая страница, не 404. У ActionEvent нет статуса (append-only), допустимость состояния не проверяется.
  3. Что читаем.
    • сессии пользователя (для истории «до и после входа», «Правило чтения» property-object-view.md): их идентификаторы по UserId;
    • страница ленты ActionEvent, где запись принадлежит пользователю напрямую (UserId) или через его сессию (SessionId), с фильтром по OccurredAt (From/To) и по ActionType (ActionTypes), отсортированная по OccurredAt (новые сверху), с пагинацией;
    • обогащение длительностью: для элементов ViewPropertyObjectDuration связанного PropertyObjectView по PropertyObjectViewId страницы (закрытые — значение, Activenull).
  4. Расчёт и маппинг. Переходов состояний нет (read-only). Маппинг поле-в-поле ActionEvent (+ PropertyObjectView.Duration) → UserActionTimelineItemModel — по таблице «Модель ответа»; FilterSnapshot читается как typed-снимок (ContractVersion = 1; при bump — обе версии).
  5. Результат. Страница ленты (Items, TotalCount, Page, PageSize) → 200 (в т.ч. пустая для неизвестного UserId). Консистентность eventual: недавно открытые просмотры без Duration (worker activity-closer ещё не закрыл), consumer-записи (*Favorites/AddToSelection) появляются после обработки их событий. Masking: SearchQuery выдаётся (Internal — по назначению карточки лида), в логах усечён.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsnone (read-only query)
External callsnone — UserId/PropertyObjectId логические ссылки, синхронно не резолвятся; display-данные объектов недвижимости добирает потребитель в catalog
Cache / projections / searchnone — прямое btree-чтение PostgreSQL (ADR-0056: без Redis; GIN по filter_snapshot нет)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400UserId = Guid.Empty, From > To, PageSize вне 1..100«Проверьте параметры запроса»no retry
UNAUTHENTICATED401нет валидного m2m service-token«Требуется аутентификация»no retry
USER_ACTIVITY_FORBIDDEN403нет permission user-activity.history.view-by-user«Недостаточно прав»no retry
— (не ошибка)200неизвестный UserId / нет действийпустая PagingResult (TotalCount = 0)
USER_ACTIVITY_UNAVAILABLE503PostgreSQL недоступен«Сервис временно недоступен»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/ (будущий путь по конвенции).

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

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

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