Skip to content

Статистика конференций и звонков

Сервер ведет постоянную статистику каждого фактического сеанса конференции: время комнаты, присутствие пользователей, повторные подключения, пиковый онлайн и отзывы после звонка. Источник истины этой подсистемы — отдельная SQLite-база stats.db.

Статистика не строится по сообщениям чата, наличию media devices или тегу комнаты в отрыве от времени. Она начинается после успешного входа первого участника в конференцию и завершается, когда из конкретного сеанса вышел последний участник.

Идентификаторы

Идентификатор Семантика
conference_tag Стабильный идентификатор комнаты. Одна комната может запускаться много раз.
conference_session_id Уникальный числовой ID одного фактического запуска комнаты. Первичный ключ conference_sessions.id.
attendance_id ID одного физического подключения command-сессии к конференции.
client_id Стабильный ID пользователя. Используется для дедупликации параллельных вкладок и reconnect.

conference_tag недостаточно для связи аналитики, записи и транскрипта: один и тот же тег может использоваться ежедневно. Для такой связи всегда применяется conference_session_id.

Один созвон, несколько запусков сервисов

conference_session_id является идентификатором созвона, а не файла, запуска рекордера или задания транскрибера. Второй call_id с той же семантикой не вводится. Пока сеанс комнаты остаётся активным, повторные recording.start и transcriber.start получают один и тот же ID от сервера, но разные job_id.

flowchart TD
    Call["Созвон: conference_session_id = 1842"]
    Call --> R1["recording job A: первый фрагмент"]
    Call --> R2["recording job B: второй фрагмент"]
    Call --> T1["transcriber job C: первая расшифровка"]
    Call --> T2["transcriber job D: вторая расшифровка"]
    R1 --> F1["Файл записи A"]
    R2 --> F2["Файл записи B"]
    T1 --> Summary["Саммари созвона 1842"]
    T2 --> Summary

Ключи не взаимозаменяемы:

  • conference_session_id группирует все материалы конкретного созвона;
  • job_id различает запуски сервиса внутри созвона;
  • blob_id идентифицирует файл, а GUID сообщения - сообщение;
  • отдельный ID саммари и его ревизия различают результаты повторной обработки, но не заменяют ID созвона.

Отношение созвона к записям и транскриптам один ко многим. Нельзя сохранять только последний recording_job_id или делать уникальным поле conference_session_id в таблице артефактов: это потеряет предыдущие фрагменты.

Контракт саммаризатора

Саммаризатор сохраняет conference_session_id из исходного CAN payload, transcript JSON или metadata записи. Например, задание постобработки может содержать следующие поля в контракте внешнего сервиса:

{
  "conference_session_id": 1842,
  "conference_tag": "teamsink",
  "source_job_ids": ["transcriber-C", "transcriber-D"],
  "source_blob_ids": ["blob-A", "blob-B"]
}

Все источники одного саммари должны относиться к одному сеансу. Список источников нужен для воспроизводимости: новое саммари после поступления дополнительной расшифровки не должно молча считаться результатом прежнего набора данных.

При поздней загрузке файла или формировании саммари после завершения звонка исходный ID не меняется. Запрещено брать для результата текущий GetActiveSessionId(conference_tag): в комнате уже может идти следующий созвон. Пример выше задаёт контракт для внешнего саммаризатора, а не новый реализованный endpoint VideoGrace. Встроенного индекса саммари и API объединённой выдачи всех материалов созвона пока нет. Проверка live transcript-сообщений также по-прежнему требует активный сеанс; этот канал нельзя считать API публикации саммари после завершения звонка.

Числовой ID уникален в пределах stats.db конкретного сервера. Агрегатор нескольких серверов хранит составной ключ (идентификатор сервера в интеграции, conference_session_id), чтобы не смешать одинаковые числа на разных установках.

Текущая граница сеанса: закрытие последнего attendance-подключения, включая учтённые сервисные подключения, либо перезапуск серверного процесса (server_restart). Пока обычный участник остаётся в комнате, включение и выключение рекордера/транскрибера эту границу не меняет. Полностью завершённая комната при новом входе получает новый ID. Сохранение одного ID через полный распад комнаты или рестарт сервера потребовало бы отдельной политики восстановления созвона.

Жизненный цикл

sequenceDiagram
    participant C as Web/native client
    participant P as Processor
    participant S as ConferenceStatistics
    participant DB as stats.db
    participant W as Recorder/Transcriber

    C->>P: connect_to_conference
    P->>P: membership accepted
    P->>S: StartAttendance(tag, title, client_id)
    S->>DB: find or create active conference_session
    S->>DB: increment participant + insert attendance
    S-->>P: attendance_id
    P->>S: GetActiveSessionId(tag)
    S-->>P: conference_session_id
    P-->>W: CAN job with conference_session_id
    C->>P: leave / disconnect
    P->>S: FinishAttendance(attendance_id)
    S->>DB: close attendance and participant interval
    alt active participants = 0
        S->>DB: close conference_session, end_reason=empty
    end
    C->>P: POST conference_review
    P->>S: SubmitReview(client_id, tag, rating, comment)
    S->>DB: upsert by session_id + client_id

Сеанс статистики создается после успешного добавления пользователя в conference membership, но до отправки интеграционных заданий Recorder и Transcriber. Поэтому worker получает уже определенный conference_session_id.

Параллельные вкладки и reconnect

Один пользователь может одновременно открыть конференцию в нескольких вкладках или восстановить command-сессию после сетевого разрыва. Эти подключения учитываются следующим образом:

  • каждая физическая command-сессия получает отдельную строку conference_attendance;
  • conference_participants содержит одну строку на пару session_id + client_id;
  • active_connections показывает число одновременных подключений пользователя;
  • join_count увеличивается при каждом входе;
  • connected_seconds растет только за интервал, когда хотя бы одно подключение пользователя активно;
  • пользователь перестает считаться online после закрытия последнего подключения.

Таким образом, две вкладки одного аккаунта не удваивают число уникальных участников и человеко-время, но остаются видны как два attendance-интервала для технической диагностики.

Хранилище

Путь задается параметром StatisticsDb секции Db:

[Db]
StatisticsDb=stats.db

Относительный путь разрешается рядом с основной базой. В типовой установке файл находится в /opt/VideoGrace/Server/db/stats.db.

conference_sessions

Поле Назначение
id conference_session_id.
conference_tag Тег комнаты.
conference_title Название комнаты на момент последней активности.
started_at Начало сеанса, Unix seconds.
ended_at Завершение; NULL для активного сеанса.
peak_participants Максимум одновременно активных уникальных пользователей.
last_activity_at Последнее изменение membership.
end_reason empty или server_restart.

conference_participants

Агрегированное присутствие уникального пользователя в конкретном сеансе.

Поле Назначение
session_id, client_id Составной первичный ключ.
first_joined_at Первый вход пользователя.
last_left_at Последний выход.
active_since Начало текущего непрерывного online-интервала.
connected_seconds Уже завершенные интервалы присутствия.
active_connections Текущее число параллельных подключений.
join_count Количество входов пользователя.

conference_attendance

Сырые интервалы отдельных command-подключений: id, session_id, client_id, joined_at, left_at. Эта таблица нужна для технической детализации и не используется как прямой источник человеко-времени, иначе параллельные вкладки завышали бы показатель.

conference_reviews

Один отзыв пользователя на один сеанс. Ограничение unique(session_id, client_id) превращает повторную отправку в обновление оценки и комментария.

Восстановление после рестарта

При открытии stats.db сервер закрывает незавершенные данные предыдущего процесса:

  1. Активный интервал каждого пользователя добавляется в connected_seconds.
  2. active_connections и active_since сбрасываются.
  3. Незакрытые attendance-интервалы получают left_at.
  4. Незакрытые conference sessions получают ended_at и end_reason=server_restart.

Это не позволяет старым строкам бесконечно увеличивать online time после аварии или планового рестарта.

Метрики административного дашборда

Сводка строится сервером при чтении GET /api/v1.0/admin_dashboard и возвращается в поле call_statistics.

Блок Поля Период
sessions total, active, total_duration_sec, average_duration_sec Последние 30 дней по started_at.
participants unique, participant_time_sec, average_per_session, peak Последние 30 дней.
feedback count, average_rating, distribution[1..5] Последние 30 дней.
daily звонки, длительность, человеко-время и оценка по UTC-дням Последние 14 календарных дней с данными.
recent последние сеансы вместе с отзывами Последние 25 сеансов независимо от 30-дневного окна.

average_per_session — среднее число уникальных строк участников на сеанс, а не средний одновременный онлайн. Для одновременного онлайна используются peak_participants конкретного сеанса и participants.peak сводки.

Пример фрагмента ответа:

{
  "call_statistics": {
    "period_days": 30,
    "sessions": {
      "total": 42,
      "active": 1,
      "total_duration_sec": 84600,
      "average_duration_sec": 2014
    },
    "participants": {
      "unique": 18,
      "participant_time_sec": 174200,
      "average_per_session": 3.4,
      "peak": 12
    },
    "feedback": {
      "count": 16,
      "average_rating": 4.63,
      "distribution": [0, 0, 1, 4, 11]
    },
    "daily": [],
    "recent": []
  }
}

Доступ к сводке

GET /api/v1.0/admin_dashboard HTTP/1.1
Authorization: Bearer <access_token>

Access token должен иметь scope api, а пользователь — право доступа к административному API. В web-клиенте данные отображаются в отдельном разделе Админ → Статистика звонков.

UI показывает:

  • количество и суммарную длительность сеансов;
  • человеко-время и число уникальных участников;
  • среднюю длительность и среднее число участников;
  • среднюю оценку;
  • дневной график звонков и человеко-часов;
  • последние 25 сеансов и текстовые отзывы.

Отзыв после звонка

Пользователь отправляет оценку от 1 до 5 и необязательный комментарий:

POST /api/v1.0/conference_review HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "conference_tag": "radio",
  "rating": 5,
  "comment": "Звук и демонстрация работали стабильно"
}

Ограничения:

  • conference_tag обязателен и не длиннее 256 байт;
  • rating находится в диапазоне 1..5;
  • комментарий не длиннее 2000 байт;
  • пользователь должен присутствовать в найденном сеансе этой комнаты;
  • сервер выбирает последний подходящий активный или завершенный не более 24 часов назад сеанс;
  • повторная отправка тем же пользователем обновляет существующий отзыв.

Ответы API:

HTTP Причина
200 Отзыв сохранен.
400 Некорректный JSON, рейтинг, тег или комментарий.
401 Нет действующего access token со scope api.
404 Подходящий сеанс с участием пользователя не найден.

Web-клиент предлагает форму только после обычного выхода из конференции, если пользователь находился в ней не менее пяти секунд и control connection остается доступным. При потере связи с сервером форма не показывается. Диалог появляется с небольшой задержкой, чтобы не перекрывать завершение media cleanup.

Связь с транскриптами и записями

conference_session_id проходит через весь artifact pipeline:

flowchart LR
    Session[stats.db conference_sessions.id]
    Job[CAN job]
    Recorder[Recorder report/artifact]
    Records[records.db]
    Transcript[Transcript message/export]
    API[Recordings API / webhook]

    Session --> Job
    Job --> Recorder
    Recorder --> Records
    Session --> Transcript
    Records --> API

Инварианты:

  • live recording job не создается, если сервер не может определить активный conference_session_id;
  • Recorder возвращает тот же ID в report и artifact metadata;
  • records.db индексирует запись вместе с ID сеанса;
  • transcript-сообщение принимается только с ID текущего активного сеанса комнаты;
  • recordings API и партнерский webhook возвращают этот ID;
  • старые артефакты без достоверной связи получают conference_session_id=0; сервер не угадывает сеанс по одному тегу.

Legacy call_stats

Таблица call_stats в history.db продолжает заполняться для обратной совместимости старых отчетов. Она не является источником нового раздела статистики и не содержит полной модели сеанса, параллельных подключений и отзывов. Новые отчеты должны использовать stats.db и conference_session_id.

Эксплуатационная проверка

Безопасные read-only команды:

sqlite3 /opt/VideoGrace/Server/db/stats.db \
  "select id, conference_tag, started_at, ended_at, peak_participants, end_reason
   from conference_sessions order by id desc limit 10;"

sqlite3 /opt/VideoGrace/Server/db/stats.db \
  "select session_id, client_id, connected_seconds, active_connections, join_count
   from conference_participants order by session_id desc, client_id;"

sqlite3 /opt/VideoGrace/Server/db/stats.db "pragma integrity_check;"

Для резервной копии работающей базы используйте SQLite backup, а не побайтовое копирование открытого файла:

sqlite3 /opt/VideoGrace/Server/db/stats.db ".backup '/var/backups/videograce/stats.db'"

Не исправляйте вручную active_connections, active_since и незакрытые sessions на работающем сервере: эти поля синхронизированы с in-memory membership.

Код и тесты

Область Реализация
Схема, lifecycle и агрегация Server/Processor/ConferenceStatistics.cpp
Интеграция с membership и artifacts Server/Processor/Processor.cpp
HTTP API Server/Processor/API.cpp
Серверный regression test Server/tests/ConferenceStatisticsTest.cpp
Admin UI web-client/src/components/admin/AdminCallStatistics.tsx
Клиентская форма отзыва web-client/src/components/meetings/ConferenceReviewDialog.tsx

Regression test проверяет создание одного сеанса, дедупликацию двух подключений одного пользователя, пиковый онлайн, корректное закрытие после последнего выхода и сохранение отзыва.