Аналитика снова пишет события в Postgres
CI / lint-test (push) Successful in 17s Details
CI / build-push (push) Successful in 20s Details

При проверке на сервере выяснилось, что psycopg-соединение не умеет executemany: батч событий уходил в fallback и терялся. Запись переведена на курсор, добавлен тест на этот путь; версия 0.7.2.
This commit is contained in:
MpakobecEx 2026-11-03 02:55:00 +05:00
parent cf14dfa80d
commit 448ab837fa
5 changed files with 71 additions and 6 deletions

View File

@ -171,7 +171,7 @@ pytest
## Версионирование ## Версионирование
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.7.1`. - Единый источник версии — `common/version.py` (`__version__`), текущая — `0.7.2`.
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`). - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
- Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`. - Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`.
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.

View File

@ -47,9 +47,9 @@ Writer = Callable[[list[EventRow], MessageCounts], Awaitable[None]]
async def _write_db(events: list[EventRow], messages: MessageCounts) -> None: async def _write_db(events: list[EventRow], messages: MessageCounts) -> None:
"""Записать батч в Postgres.""" """Записать батч в Postgres."""
async with get_pool().connection() as conn: async with get_pool().connection() as conn, conn.cursor() as cur:
if events: if events:
await conn.executemany( await cur.executemany(
""" """
INSERT INTO analytics.events INSERT INTO analytics.events
(event_time, event_type, user_id, username, chat_id, payload, bot_version, prod) (event_time, event_type, user_id, username, chat_id, payload, bot_version, prod)
@ -71,7 +71,7 @@ async def _write_db(events: list[EventRow], messages: MessageCounts) -> None:
) )
for (day, user_id, chat_id), (username, count) in messages.items(): for (day, user_id, chat_id), (username, count) in messages.items():
await conn.execute( await cur.execute(
""" """
INSERT INTO analytics.messages_daily (day, user_id, username, chat_id, count) INSERT INTO analytics.messages_daily (day, user_id, username, chat_id, count)
VALUES (%s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s)

View File

@ -7,4 +7,4 @@
from __future__ import annotations from __future__ import annotations
__version__ = "0.7.1" __version__ = "0.7.2"

View File

@ -60,6 +60,51 @@ class FakeWriter:
self.messages[key] = list(value) self.messages[key] = list(value)
class FakeCursor:
"""Курсор psycopg: запоминает вызовы."""
def __init__(self) -> None:
self.calls: list[tuple[str, Any, Any]] = []
async def executemany(self, query: str, rows: list[Any]) -> None:
self.calls.append(("executemany", query, rows))
async def execute(self, query: str, params: Any) -> None:
self.calls.append(("execute", query, params))
async def __aenter__(self) -> FakeCursor:
return self
async def __aexit__(self, *args: Any) -> None:
return None
class FakeConnection:
"""Соединение psycopg с курсором."""
def __init__(self) -> None:
self.cursor_obj = FakeCursor()
def cursor(self) -> FakeCursor:
return self.cursor_obj
async def __aenter__(self) -> FakeConnection:
return self
async def __aexit__(self, *args: Any) -> None:
return None
class FakePool:
"""Пул с одним соединением."""
def __init__(self) -> None:
self.conn = FakeConnection()
def connection(self) -> FakeConnection:
return self.conn
def _settings(**overrides: Any) -> Settings: def _settings(**overrides: Any) -> Settings:
return Settings(_env_file=None, bot_token="1:a", **overrides) return Settings(_env_file=None, bot_token="1:a", **overrides)
@ -230,6 +275,26 @@ async def test_disabled_analytics_ignores_events() -> None:
assert fake.messages == {} assert fake.messages == {}
# --- запись в Postgres ---
async def test_write_db_uses_cursor_executemany(monkeypatch: pytest.MonkeyPatch) -> None:
from common.analytics import client as analytics_client
pool = FakePool()
monkeypatch.setattr(analytics_client, "get_pool", lambda: pool)
await analytics_client._write_db(
[[datetime.now(UTC), "command", 1, "vasya", 100, {"command": "инфо"}, "0.7.2", False]],
{(datetime.now(UTC).date(), 1, 100): ["vasya", 3]},
)
calls = pool.conn.cursor_obj.calls
assert calls[0][0] == "executemany"
assert calls[1][0] == "execute"
assert "messages_daily" in calls[1][1]
# --- деградация без Postgres --- # --- деградация без Postgres ---

View File

@ -7,4 +7,4 @@ from common import __version__
def test_version_is_set(): def test_version_is_set():
assert __version__ assert __version__
assert __version__ == "0.7.1" assert __version__ == "0.7.2"