Статистика конференций и звонков
Сервер ведет постоянную статистику каждого фактического сеанса конференции: время комнаты, присутствие пользователей, повторные подключения, пиковый онлайн и отзывы после звонка. Источник истины этой подсистемы — отдельная 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 сервер закрывает незавершенные данные предыдущего процесса:
- Активный интервал каждого пользователя добавляется в
connected_seconds. active_connectionsиactive_sinceсбрасываются.- Незакрытые attendance-интервалы получают
left_at. - Незакрытые 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 проверяет создание одного сеанса, дедупликацию двух подключений одного пользователя, пиковый онлайн, корректное закрытие после последнего выхода и сохранение отзыва.