diff --git a/app_stats/app_metrics.json b/app_stats/app_metrics.json
new file mode 100644
index 0000000..07ce86a
--- /dev/null
+++ b/app_stats/app_metrics.json
@@ -0,0 +1,236 @@
+{
+ "generated_at": "2026-06-16T17:30:19",
+ "cur_year": 2026,
+ "prev_year": 2025,
+ "period_label": "с 1 января по 15 июня",
+ "range": {
+ "start": "2026-01-01",
+ "end": "2026-06-15"
+ },
+ "metrics": [
+ {
+ "key": "my_services",
+ "label": "Мои услуги",
+ "prev": 1459571,
+ "cur": 1769470,
+ "growth": 0.21232197680003234,
+ "is_new": false
+ },
+ {
+ "key": "traffic",
+ "label": "Детализация трафика",
+ "prev": 1079736,
+ "cur": 1271563,
+ "growth": 0.17766102084213178,
+ "is_new": false
+ },
+ {
+ "key": "payments",
+ "label": "Платежи",
+ "prev": 553808,
+ "cur": 730185,
+ "growth": 0.3184804119839367,
+ "is_new": false
+ },
+ {
+ "key": "orders",
+ "label": "Заявки",
+ "prev": 635621,
+ "cur": 826255,
+ "growth": 0.2999177182629271,
+ "is_new": false
+ },
+ {
+ "key": "loyalty",
+ "label": "Лояльность",
+ "prev": 464365,
+ "cur": 470969,
+ "growth": 0.014221571393192856,
+ "is_new": false
+ },
+ {
+ "key": "pay",
+ "label": "Оплата",
+ "prev": 302510,
+ "cur": 337103,
+ "growth": 0.11435324452084229,
+ "is_new": false
+ },
+ {
+ "key": "billing_detail",
+ "label": "Детали счета",
+ "prev": 358290,
+ "cur": 496915,
+ "growth": 0.3869072539004717,
+ "is_new": false
+ },
+ {
+ "key": "viktorina",
+ "label": "Викторина KT Club",
+ "prev": 298475,
+ "cur": 213879,
+ "growth": -0.28342742273222216,
+ "is_new": false
+ },
+ {
+ "key": "partners",
+ "label": "Акции партнеров",
+ "prev": 94639,
+ "cur": 197009,
+ "growth": 1.08168936696288,
+ "is_new": false
+ },
+ {
+ "key": "tv_plus",
+ "label": "TV+",
+ "prev": 95647,
+ "cur": 64104,
+ "growth": -0.32978556567377965,
+ "is_new": false
+ },
+ {
+ "key": "boosters",
+ "label": "Бустеры",
+ "prev": 53649,
+ "cur": 121065,
+ "growth": 1.2566124252082984,
+ "is_new": false
+ },
+ {
+ "key": "roaming",
+ "label": "Роуминг",
+ "prev": 39200,
+ "cur": 22160,
+ "growth": -0.4346938775510204,
+ "is_new": false
+ },
+ {
+ "key": "pereoform",
+ "label": "Переоформление",
+ "prev": 23537,
+ "cur": 34570,
+ "growth": 0.46875132769681777,
+ "is_new": false
+ },
+ {
+ "key": "aitu_music",
+ "label": "Aitu Music",
+ "prev": 0,
+ "cur": 8651,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "online_booking",
+ "label": "Онлайн очередь",
+ "prev": 5421,
+ "cur": 22144,
+ "growth": 3.084855192768862,
+ "is_new": false
+ },
+ {
+ "key": "my_docs",
+ "label": "Мои документы",
+ "prev": 0,
+ "cur": 53376,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "dz_statement",
+ "label": "Справка о ДЗ",
+ "prev": 0,
+ "cur": 132795,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "new_boosters_roaming_kcell",
+ "label": "Новая линейка бустеров и роумингов Кселл",
+ "prev": 0,
+ "cur": 28626,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "adsl",
+ "label": "ADSL отключение услуги",
+ "prev": 0,
+ "cur": 69,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "law_and_order",
+ "label": "Закон и порядок",
+ "prev": 0,
+ "cur": 1555,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "acs",
+ "label": "ACS",
+ "prev": 0,
+ "cur": 9154,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "kaspi_freedom_pay",
+ "label": "Прием платежей через Freedom и Kaspi",
+ "prev": 0,
+ "cur": 61481,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "csat",
+ "label": "CSAT",
+ "prev": 0,
+ "cur": 2486,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "multicustomer",
+ "label": "Мультикастомер",
+ "prev": 0,
+ "cur": 164,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "tv_plus_setup",
+ "label": "Настройка TV+",
+ "prev": 0,
+ "cur": 4545,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "static_ip",
+ "label": "Статический IP",
+ "prev": 0,
+ "cur": 108,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "turbo_button",
+ "label": "Turbo кнопка",
+ "prev": 0,
+ "cur": 4312,
+ "growth": null,
+ "is_new": true
+ },
+ {
+ "key": "real_estate_docs",
+ "label": "Справка о недвижимости",
+ "prev": 0,
+ "cur": 78,
+ "growth": null,
+ "is_new": true
+ }
+ ]
+}
diff --git a/app_stats/index.html b/app_stats/index.html
new file mode 100644
index 0000000..984935e
--- /dev/null
+++ b/app_stats/index.html
@@ -0,0 +1,560 @@
+
+
+
+
+
+Метрики МП — Казахтелеком 2026
+
+
+
+
+
+
+
+
+
+
KT
+
Метрики МП
+
Казахтелеком · 2026
+
+
+
+
+
+
+
+
+
+
+
+
Загрузка данных…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app_stats/Результаты по метрикам 202606.xlsx b/app_stats/Результаты по метрикам 202606.xlsx
new file mode 100644
index 0000000..bafefe4
Binary files /dev/null and b/app_stats/Результаты по метрикам 202606.xlsx differ
diff --git a/index.html b/index.html
index 4d7af66..1984ebc 100644
--- a/index.html
+++ b/index.html
@@ -280,6 +280,7 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
diff --git a/updater/run_update.bat b/updater/run_update.bat
index 2ac40f1..5b2cc0e 100644
--- a/updater/run_update.bat
+++ b/updater/run_update.bat
@@ -13,8 +13,16 @@ if exist "venv\Scripts\python.exe" (
set "PYEXE=python"
)
+REM 1) KPI dashboard (drb_iliyas_kpi_2026.csv)
"%PYEXE%" "%~dp0update_kpi.py"
-set "RC=%ERRORLEVEL%"
+set "RC_KPI=%ERRORLEVEL%"
-echo Exit code: %RC%
+REM 2) Метрики МП (app_stats/app_metrics.json)
+"%PYEXE%" "%~dp0update_app_metrics.py"
+set "RC_APP=%ERRORLEVEL%"
+
+echo KPI exit code: %RC_KPI% App-metrics exit code: %RC_APP%
+
+REM Ненулевой код, если упал хотя бы один
+set /a RC=%RC_KPI%+%RC_APP%
exit /b %RC%
diff --git a/updater/update_app_metrics.py b/updater/update_app_metrics.py
new file mode 100644
index 0000000..ec5462a
--- /dev/null
+++ b/updater/update_app_metrics.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Ежедневное обновление статистики «Метрики МП» из Impala.
+
+Сравнивает использование функций мобильного приложения за текущий год
+(с 1 января по ВЧЕРАШНИЙ день) с аналогичным периодом прошлого года.
+Результат пишется в ../app_stats/app_metrics.json и пушится в ветку pages —
+его читает страница app_stats/index.html.
+
+Подключение к Impala, конфиг и git-push переиспользуются из update_kpi.py.
+"""
+
+import sys
+import json
+import datetime as dt
+from pathlib import Path
+
+import update_kpi as base # общие функции: load_config, connect_impala, git_commit_push, ...
+
+log = base.log
+REPO_DIR = base.REPO_DIR
+OUT_PATH = REPO_DIR / "app_stats" / "app_metrics.json"
+OUT_REL = "app_stats/app_metrics.json"
+
+# ─── Метрики: ключ в SQL → человекочитаемое название (в порядке SELECT) ───
+METRICS = [
+ ("my_services", "Мои услуги"),
+ ("traffic", "Детализация трафика"),
+ ("payments", "Платежи"),
+ ("orders", "Заявки"),
+ ("loyalty", "Лояльность"),
+ ("pay", "Оплата"),
+ ("billing_detail", "Детали счета"),
+ ("viktorina", "Викторина KT Club"),
+ ("partners", "Акции партнеров"),
+ ("tv_plus", "TV+"),
+ ("boosters", "Бустеры"),
+ ("roaming", "Роуминг"),
+ ("pereoform", "Переоформление"),
+ ("aitu_music", "Aitu Music"),
+ ("online_booking", "Онлайн очередь"),
+ ("my_docs", "Мои документы"),
+ ("dz_statement", "Справка о ДЗ"),
+ ("new_boosters_roaming_kcell", "Новая линейка бустеров и роумингов Кселл"),
+ ("adsl", "ADSL отключение услуги"),
+ ("law_and_order", "Закон и порядок"),
+ ("acs", "ACS"),
+ ("kaspi_freedom_pay", "Прием платежей через Freedom и Kaspi"),
+ ("csat", "CSAT"),
+ ("multicustomer", "Мультикастомер"),
+ ("tv_plus_setup", "Настройка TV+"),
+ ("static_ip", "Статический IP"),
+ ("turbo_button", "Turbo кнопка"),
+ ("real_estate_docs", "Справка о недвижимости"),
+]
+
+_MONTHS_RU = ["", "января", "февраля", "марта", "апреля", "мая", "июня",
+ "июля", "августа", "сентября", "октября", "ноября", "декабря"]
+
+
+def date_range(today: dt.date | None = None):
+ """Возвращает (cur_year, prev_year, start_cur, end_cur, start_prev, end_prev) как date."""
+ today = today or dt.date.today()
+ end_cur = today - dt.timedelta(days=1) # вчера
+ cur_year = today.year
+ prev_year = cur_year - 1
+ start_cur = dt.date(cur_year, 1, 1)
+ start_prev = dt.date(prev_year, 1, 1)
+ # та же дата прошлого года; подстраховка на 29 февраля
+ try:
+ end_prev = dt.date(prev_year, end_cur.month, end_cur.day)
+ except ValueError:
+ end_prev = dt.date(prev_year, end_cur.month, 28)
+ return cur_year, prev_year, start_cur, end_cur, start_prev, end_prev
+
+
+def build_sql(start_cur, end_cur, start_prev, end_prev) -> str:
+ return f"""
+with t as (
+ select round(report_period_id/100) as report_year,
+ count(case when event_type = 'OPENWSCREENMYSERVICES' then 1 end) as my_services,
+ count(case when event_type = 'OPENWINDOWDETALIZTION' then 1 end) as traffic,
+ count(case when event_type = 'OPENWINDOWPAYMENT' then 1 end) as payments,
+ count(case when event_type = 'OPENSCREENAPPEALS' then 1 end) as orders,
+ count(case when event_type in ('banner_auth', 'banner_unauth', 'loyalty_banner_slider_auth', 'loyalty_banner_slider_unauth', 'get_bonus_opened', 'bonus_opened','promo_partners_opened','company_promo_opened') then 1 end) as loyalty,
+ count(case when event_type = 'WINDOWPAYMENT' then 1 end) as pay,
+ count(case when event_type = 'OPENWINDOWBILLING' then 1 end) as billing_detail,
+ count(case when event_type = 'game_page' then 1 end) as viktorina,
+ count(case when event_type = 'promo_partners_opened' then 1 end) as partners,
+ count(case when event_type = 'OPENWINDOWTVPLUS' then 1 end) as tv_plus,
+ count(case when event_type = 'MOBCONNECTIONOPENWINDOWADDITIONALTRAFFIC' then 1 end) as boosters,
+ count(case when event_type = 'OPENWINDOWROAMING' then 1 end) as roaming,
+ count(case when event_type = 'reregistration_comm_start' then 1 end) as pereoform,
+ count(case when event_type = 'aitu_music_banner_clicked' then 1 end) as aitu_music,
+ count(case when event_type = 'ONLINE_BOOKING_SERVICES' then 1 end) as online_booking,
+ count(case when event_type in ('EMPTYLISTDOCS', 'HASLISTDOCS') then 1 end) as my_docs,
+ count(case when event_type = 'PDFSTATEMENT' then 1 end) as dz_statement,
+ count(case when event_type in ('booster_success_screen_kcell', 'ROAMINGPACKAGEMOBILEKCELL') then 1 end) as new_boosters_roaming_kcell,
+ count(case when event_type = 'law_and_order_service_clicked' then 1 end) as law_and_order,
+ count(case when event_type = 'ACS_DEVICE_SELECTION_OPEN' then 1 end) as acs,
+ count(case when event_type in ('PAYMENTWASSUCCESSFULFREEDOM', 'PAYWITHKASPI') then 1 end) as kaspi_freedom_pay,
+ count(case when event_type = 'csat_screen_sent' then 1 end) as csat,
+ count(case when event_type = 'multicustomer_completed_screen_viewed' then 1 end) as multicustomer,
+ count(case when event_type = 'tv_plus_setup_success_viewed' then 1 end) as tv_plus_setup,
+ count(case when event_type = 'static_ip_connect_success_viewed' then 1 end) as static_ip,
+ count(case when event_type = 'turbo_activation_success_viewed' then 1 end) as turbo_button,
+ count(case when event_type = 'real_estate_docs_screen_shown' then 1 end) as real_estate_docs
+ from drb.drb_iliyas_amplitude_metrics_full
+ where entry_date between '{start_cur:%Y-%m-%d}' and '{end_cur:%Y-%m-%d}'
+ or entry_date between '{start_prev:%Y-%m-%d}' and '{end_prev:%Y-%m-%d}'
+ group by 1
+)
+, a as (
+ select year(created_at) as report_year, count(order_id) as adsl
+ from telecomkz.telecomkz_retention_service_prod_tariff_change_validations
+ group by 1
+)
+select t.report_year, my_services, traffic, payments, orders, loyalty, pay, billing_detail, viktorina, partners, tv_plus, boosters, roaming, pereoform, aitu_music, online_booking, my_docs, dz_statement, new_boosters_roaming_kcell, adsl, law_and_order, acs, kaspi_freedom_pay, csat, multicustomer,
+tv_plus_setup, static_ip, turbo_button, real_estate_docs
+from t
+left join a on t.report_year = a.report_year
+order by t.report_year
+""".strip()
+
+
+def fetch_by_year(conn, sql):
+ cur = conn.cursor()
+ log.info("Выполнение запроса метрик МП...")
+ cur.execute(sql)
+ rows = cur.fetchall()
+ names = [d[0].lower() for d in cur.description]
+ cur.close()
+ idx = {n: i for i, n in enumerate(names)}
+ by_year = {}
+ for r in rows:
+ year = int(round(float(r[idx["report_year"]])))
+ by_year[year] = {name: r[idx[name]] for name in idx}
+ log.info("Получено годовых строк: %s", sorted(by_year))
+ return by_year, idx
+
+
+def _num(v):
+ return int(v) if v is not None else 0
+
+
+def build_payload(by_year, cur_year, prev_year, start_cur, end_cur):
+ cur_row = by_year.get(cur_year, {})
+ prev_row = by_year.get(prev_year, {})
+
+ metrics = []
+ for key, label in METRICS:
+ cur_v = _num(cur_row.get(key))
+ prev_v = _num(prev_row.get(key))
+ is_new = prev_v == 0
+ growth = None if is_new else (cur_v - prev_v) / prev_v
+ metrics.append({
+ "key": key, "label": label,
+ "prev": prev_v, "cur": cur_v,
+ "growth": growth, "is_new": is_new,
+ })
+
+ period_label = (f"с 1 января по {end_cur.day} {_MONTHS_RU[end_cur.month]}")
+ return {
+ "generated_at": dt.datetime.now().isoformat(timespec="seconds"),
+ "cur_year": cur_year,
+ "prev_year": prev_year,
+ "period_label": period_label,
+ "range": {"start": f"{start_cur:%Y-%m-%d}", "end": f"{end_cur:%Y-%m-%d}"},
+ "metrics": metrics,
+ }
+
+
+def write_if_changed(payload) -> bool:
+ text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
+ old = ""
+ if OUT_PATH.exists():
+ old = OUT_PATH.read_text(encoding="utf-8")
+
+ # Сравниваем без учёта generated_at (чтобы не коммитить, если данные те же)
+ def strip_ts(s):
+ try:
+ d = json.loads(s)
+ d.pop("generated_at", None)
+ return json.dumps(d, ensure_ascii=False, sort_keys=True)
+ except Exception:
+ return s
+
+ if strip_ts(old) == strip_ts(text):
+ log.info("Метрики МП не изменились — коммит не требуется.")
+ return False
+
+ OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ OUT_PATH.write_text(text, encoding="utf-8")
+ log.info("app_metrics.json обновлён (%d метрик).", len(payload["metrics"]))
+ return True
+
+
+def main() -> int:
+ log.info("=" * 60)
+ log.info("Старт обновления Метрик МП")
+ cfg = base.load_config()
+
+ cur_year, prev_year, start_cur, end_cur, start_prev, end_prev = date_range()
+ log.info("Период: %s..%s (тек.) и %s..%s (пред.)",
+ start_cur, end_cur, start_prev, end_prev)
+ sql = build_sql(start_cur, end_cur, start_prev, end_prev)
+
+ base.patch_thrift_ssl()
+ conn = base.connect_impala(cfg)
+ try:
+ by_year, _ = fetch_by_year(conn, sql)
+ finally:
+ try:
+ conn.close()
+ except Exception:
+ pass
+
+ if cur_year not in by_year:
+ log.error("В ответе нет данных за %s — JSON НЕ перезаписан.", cur_year)
+ return 1
+
+ payload = build_payload(by_year, cur_year, prev_year, start_cur, end_cur)
+ if write_if_changed(payload):
+ base.git_commit_push(cfg, [OUT_REL],
+ f"data: update app metrics {dt.date.today():%Y-%m-%d}")
+ log.info("Готово (Метрики МП).")
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except Exception as e: # noqa: BLE001
+ log.exception("ОШИБКА (Метрики МП): %s", e)
+ sys.exit(1)
diff --git a/updater/update_kpi.py b/updater/update_kpi.py
index ab468ed..4e06139 100644
--- a/updater/update_kpi.py
+++ b/updater/update_kpi.py
@@ -241,26 +241,27 @@ def push_url(cfg: dict, mask_token: str):
return url
-def commit_and_push(cfg: dict):
+def git_commit_push(cfg: dict, rel_paths, message: str):
+ """Коммитит указанные файлы (пути относительно корня репо) и пушит в ветку.
+
+ Переиспользуется и для KPI, и для метрик МП. Если изменений нет — выходит молча.
+ """
git = cfg.get("git", {}) or {}
branch = git.get("branch", "pages")
token = (git.get("token") or "")
url = push_url(cfg, token)
- # Коммитим только CSV — daily-диффы остаются чистыми.
- _run_git(["add", "--", CSV_PATH.name])
+ _run_git(["add", "--", *rel_paths])
- rc, out = _run_git(["status", "--porcelain", "--", CSV_PATH.name], check=True)
+ rc, out = _run_git(["status", "--porcelain", "--", *rel_paths], check=True)
if not out.strip():
- log.info("Нет изменений CSV для коммита.")
+ log.info("Нет изменений (%s) для коммита.", ", ".join(rel_paths))
return
- msg = f"data: update KPI {datetime.now():%Y-%m-%d}"
- _run_git(["commit", "-m", msg])
+ _run_git(["commit", "-m", message])
# Пуш с автоматическим rebase при гонке (веб-приложение тоже пушит ai-cache.json через API)
for attempt in range(1, 4):
- # подтянуть свежий remote и переставить наш коммит сверху
_run_git(["fetch", url, branch], check=True, mask=token or None)
# --autostash: не падать, если в дереве есть посторонние незакоммиченные правки
_run_git(["rebase", "--autostash", "FETCH_HEAD"], check=True)
@@ -272,6 +273,11 @@ def commit_and_push(cfg: dict):
raise RuntimeError("Не удалось запушить изменения после 3 попыток.")
+def commit_and_push(cfg: dict):
+ # Коммитим только CSV — daily-диффы остаются чистыми.
+ git_commit_push(cfg, [CSV_PATH.name], f"data: update KPI {datetime.now():%Y-%m-%d}")
+
+
# ═══════════════════════ main ════════════════════════════════
def main() -> int:
log.info("=" * 60)