Problem - GlitchTip Issues = 0 за всё время, хотя backend + frontend SDK интегрированы (PR #207, #208). - Старый Sentry.io продолжает получать события — user видит уведомления оттуда. Root cause - frontend/Dockerfile не имеет ARG NEXT_PUBLIC_GLITCHTIP_DSN → `npm run build` бьёт с пустым env var → Next.js инлайнит undefined → SDK init guard `if (dsn)` skips. Chrome-devtools check на prod bundle подтвердил: ни в одном из 9 chunks DSN-строка не запечена; `i.env.NEXT_PUBLIC_GLITCHTIP_DSN` evaluates to undefined. - .forgejo/workflows/deploy.yml build-frontend не передавал build-args. - На VPS backend/.env.runtime содержит legacy SENTRY_DSN=...@sentry.io/... config.py:_promote_legacy_sentry_dsn слепо промоутит его в glitchtip_dsn → SDK шлёт в чужой Sentry. GLITCHTIP_DSN там не задан. - deploy.yml SSH-скрипт никогда не редактировал SENTRY_DSN/GLITCHTIP_DSN в .env.runtime. Solution 1. frontend/Dockerfile: ARG NEXT_PUBLIC_GLITCHTIP_DSN + NEXT_PUBLIC_ENVIRONMENT с пустыми defaults, ENV-mirror перед `npm run build`. Локальный build без build-args работает по-прежнему (no-op DSN). 2. .forgejo/workflows/deploy.yml: - build-frontend: `build-args` передаёт `secrets.GLITCHTIP_FRONTEND_DSN` + NEXT_PUBLIC_ENVIRONMENT=production. Инвалидирует cache → frontend image пересоберётся (expected). - deploy step: GLITCHTIP_BACKEND_DSN через secret, в SSH-скрипте: a) `sed -i '/^SENTRY_DSN=/d' backend/.env.runtime` — снести legacy b) upsert GLITCHTIP_DSN (sed/printf) тем же паттерном что SENTRY_RELEASE c) `compose up -d --force-recreate --no-deps backend worker beat` — обычный `up -d` не перечитывает env_file без image change. 3. backend/app/core/config.py: _promote_legacy_sentry_dsn ужесточён — принимает SENTRY_DSN только если host == errors.gendsgn.ru. Для других URLs (sentry.io) выдаёт UserWarning и НЕ промоутит. Anti-regression на случай если SENTRY_DSN снова окажется в .env.runtime после ручного вмешательства. Required Forgejo secrets (Settings → Actions → Secrets) - GLITCHTIP_BACKEND_DSN = https://3d6e291003e142458957490c83559867@errors.gendsgn.ru/1 - GLITCHTIP_FRONTEND_DSN = https://5d7bc85e300c4e80a8554ccc818ff56d@errors.gendsgn.ru/2 DSNs публичны (видны в browser bundle) — secrets ради build-time injection, не для конфиденциальности. Если secrets не заданы → deploy succeeds, SDK no-op, без регрессии. Test plan - Verify Forgejo deploy.yml зелёный после merge - chrome-devtools: открыть gendsgn.ru → search bundle на DSN string → должна быть запечена строка errors.gendsgn.ru/2 - Trigger frontend error → POST к errors.gendsgn.ru/api/2/envelope/ - Backend: curl на endpoint вызывающий 500 → событие в GlitchTip backend project - GlitchTip dashboard https://errors.gendsgn.ru/gendesign/issues — Issues > 0 References - vault: meta/00_credentials.md (DSNs + incident notes 2026-05-16) - vault: decisions/Dec_GlitchTip_Frontend_Sentry_SDK.md (env contract) - vault: fixes/fixes-MOC.md (#204 backend SDK init)
134 lines
8.2 KiB
Python
134 lines
8.2 KiB
Python
import os
|
||
import warnings
|
||
|
||
from pydantic import model_validator
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||
|
||
database_url: str = "postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign"
|
||
redis_url: str = "redis://localhost:6379/0"
|
||
cors_origins: list[str] = ["http://localhost:3000"]
|
||
# GlitchTip error tracking (Sentry-compatible self-hosted).
|
||
# Формат DSN: https://<key>@errors.gendsgn.ru/<project_id>
|
||
# Пустая строка / None = SDK не инициализируется (no-op).
|
||
glitchtip_dsn: str | None = None
|
||
glitchtip_traces_sample_rate: float = 0.05
|
||
environment: str = "dev"
|
||
|
||
@model_validator(mode="after")
|
||
def _promote_legacy_sentry_dsn(self) -> "Settings":
|
||
"""Backward-compat: legacy SENTRY_DSN → glitchtip_dsn ТОЛЬКО для self-hosted.
|
||
|
||
Старый VPS .env.runtime мог содержать SENTRY_DSN=https://...@sentry.io/...
|
||
(legacy SaaS Sentry). Промоутить его НЕЛЬЗЯ — события пойдут в чужой проект.
|
||
Принимаем только URLs указывающие на наш errors.gendsgn.ru host.
|
||
"""
|
||
if not self.glitchtip_dsn:
|
||
legacy = os.getenv("SENTRY_DSN")
|
||
if legacy:
|
||
if "errors.gendsgn.ru" in legacy:
|
||
warnings.warn(
|
||
"SENTRY_DSN is set but ignored by pydantic; rename to GLITCHTIP_DSN. "
|
||
"Auto-promoting for backward compat.",
|
||
DeprecationWarning,
|
||
stacklevel=2,
|
||
)
|
||
self.glitchtip_dsn = legacy
|
||
else:
|
||
warnings.warn(
|
||
f"SENTRY_DSN points to non-GlitchTip host "
|
||
f"({legacy.split('@', 1)[-1][:40]}...) — ignoring. "
|
||
"Set GLITCHTIP_DSN explicitly.",
|
||
UserWarning,
|
||
stacklevel=2,
|
||
)
|
||
return self
|
||
|
||
# External APIs (Stage 2)
|
||
rosreestr_pkk_base_url: str = "https://pkk.rosreestr.ru/api/features/1"
|
||
overpass_url: str = "https://overpass-api.de/api/interpreter"
|
||
|
||
# Scraper schedule (наш.дом.рф kn-API).
|
||
# Crontab format: "minute hour day_of_month month day_of_week" (Celery crontab).
|
||
# Default: каждый понедельник в окне 04:00–05:00 по МСК.
|
||
scrape_kn_cron: str = "15 4 * * mon"
|
||
# Random delay window in seconds added on top of scheduled start so the worker
|
||
# does not hit DOM.РФ at exactly the same minute every cycle. 0 = disabled.
|
||
scrape_kn_jitter_seconds: int = 1800
|
||
# Default region(s) for scheduled sweeps. Comma-separated.
|
||
scrape_kn_default_regions: str = "66"
|
||
# Path to a pre-captured Playwright storage_state.json (committed in repo,
|
||
# used by worker to skip cold-start WAF challenge).
|
||
scrape_kn_state_path: str = "data/playwright_state.json"
|
||
# Token to authorize ad-hoc /api/v1/admin/scrape/* trigger calls.
|
||
# Empty string = endpoint disabled.
|
||
scrape_admin_token: str = ""
|
||
|
||
# NSPD-scraper (Playwright) УДАЛЁН 2026-05-11. Сменён на bulk geo-fetcher
|
||
# через rosreestr2coord — запускается вручную через /admin/scrape/geo.
|
||
# Settings оставлены deprecated на случай отката (можно удалить позже).
|
||
scrape_nspd_cron: str = "30 3 20 2,5,8,11 *" # DEPRECATED
|
||
scrape_nspd_default_regions: str = "66" # DEPRECATED
|
||
scrape_nspd_rate_ms: int = 600 # DEPRECATED — see nspd_lite_rate_ms
|
||
|
||
# Objective.ru API (api.objctv.ru) — платная аналитика первички/вторички.
|
||
# Лежит наряду с DOM.РФ kn + rosreestr CSV как 3-й источник истины с
|
||
# самой богатой моделью (per-flat per-day, escrow/банк-долг, ЕГРН-ИНН).
|
||
# Получить ключ: личный закрытый ApiKey от Объектива (тариф per-территория).
|
||
# Tank-схема:
|
||
# 1. GET /Users/User/GetToken?apiKey=<KEY> → JSON с Bearer-токеном
|
||
# 2. GET /v2/Report/GetReport?... + Authorization: Bearer <token>
|
||
objective_api_key: str | None = None
|
||
objective_base_url: str = "https://api.objctv.ru"
|
||
# Группа = «настройка территории» внутри Объектива. Чаще всего совпадает с
|
||
# городом/областью. Доступные группы определяются тарифом ключа.
|
||
objective_default_group: str = "Екатеринбург"
|
||
# Список групп для еженедельного auto-sync (csv). Эмпирически проверено
|
||
# 2026-05-10 что эти группы доступны на нашем тарифе:
|
||
# - "Свердловская область" → 1626 corp_sum/3д (= ЕКБ + спутники + регион)
|
||
# - "Челябинск" → 355
|
||
# - "Тюмень" → 985
|
||
# - "Пермь" → 423
|
||
# Стратегия Variant B (включить наш sync параллельно Антоновому ETL):
|
||
# Антон тянет только Екб (303k lots). Наш sync даёт +30k Свердл.обл
|
||
# за пределами Екб + ~700k квартир УрФО (Челябинск, Тюмень, Пермь).
|
||
# Итог в PG: ~1М квартир уральского + западносибирского рынка.
|
||
# Между группами task делает паузу _OBJECTIVE_INTER_GROUP_DELAY = 30с
|
||
# чтобы не упереться в rate-limit Объектива.
|
||
objective_sync_groups: str = "Свердловская область,Челябинск,Тюмень,Пермь"
|
||
# Токен Bearer короткоживущий (предположительно 30 мин — уточнить).
|
||
# Кешируем в Redis с этим TTL (с запасом 5 мин).
|
||
objective_token_ttl_seconds: int = 25 * 60
|
||
# Расписание sync — еженедельно по вторникам в 06:00 МСК.
|
||
# ПОЧЕМУ ВТОРНИК (а не понедельник как раньше): Антон тянет в понедельник,
|
||
# к утру вторника его SQLite уже обновлён — наш sync захватывает свежий
|
||
# snapshot за тот же отчётный период.
|
||
objective_sync_cron: str = "0 6 * * tue"
|
||
|
||
# NSPD lite (urllib через WAF) — feature toggle для переключения с
|
||
# Playwright-based nspd_kn.py на минималистичный nspd_lite.py.
|
||
# 2026-05-11: эмпирически проверено что urllib проходит WAF nspd.gov.ru
|
||
# (TLS-fingerprint stdlib не блокируется). Старый Playwright-путь
|
||
# остаётся как fallback на случай возврата WAF-проблем.
|
||
use_nspd_lite: bool = True
|
||
# Default rate-limit для nspd_lite fetcher (мс между запросами).
|
||
nspd_lite_rate_ms: int = 600
|
||
|
||
# ETL Антоновского /sf/api/* SQLite → нашу PG (objective_* таблицы).
|
||
# На проде монтируется bind-mount-ом docker-compose:
|
||
# /opt/gendesign/site-finder/analysis.db -> /data/anton-sqlite/analysis.db
|
||
# Триггерится вручную через POST /api/v1/admin/scrape/objective.
|
||
# Для локальной разработки — указать абсолютный путь к скачанному snapshot:
|
||
# OBJECTIVE_ANTON_SQLITE_PATH=C:/Users/user/source/repos/gendesign/sf_anton_snapshot.db
|
||
objective_anton_sqlite_path: str = "/data/anton-sqlite/analysis.db"
|
||
|
||
# OpenRouteService API (https://openrouteservice.org/dev/#/signup).
|
||
# Free tier: 2000 запросов/день. Используется в /parcels/{cad}/isochrones.
|
||
# Если не задан — endpoint вернёт 503 с инструкцией по регистрации.
|
||
openrouteservice_api_key: str = ""
|
||
|
||
|
||
settings = Settings()
|