happ
Happ Platform → NetHunt CRM + Slack. One integration, one set of NetHunt credentials, several webhook endpoints — grouped here because they all write the same CRM from Happ-owned event sources. Replaces the former Make.com scenarios.
Endpoints
All are POST under the gateway prefix /happ and share the same webhook_secret body field (injected by cf-proxy; enforced only when WEBHOOK_SECRET is set).
| Endpoint | Source | Does |
|---|---|---|
/happ/webhook/platform-events | platform-api lifecycle events | Upserts the funnel record (by email), records payments, Slack alerts |
/happ/webhook/leads | public marketing form | Creates a contact, then a source-routed lead record |
/happ/webhook/sales-demo-events | platform-api sales demo (IG) | Find-or-create funnel card by igsid, comment per event, Slack |
/happ/webhook/va-alerts | external VA team | Posts an alert to Slack #va-alerts. No CRM write |
/happ/webhook/prod-alerts | Alertmanager/Grafana, Sentry | Normalizes, collapses repeats, forwards to SHEV POST /api/alerts |
/happ/webhook/shop-orders | merch store shop.happ.tools | Validates a signed order, posts it to Slack. No CRM write. Own HMAC |
/happ/logs/query | SHEV agent (prod_logs) | Read-only Loki query by allowlist, redacted. Own PROD_LOGS_TOKEN |
platform-events
Upserts one NetHunt "Воронка Платформа" record per user (keyed on email), advancing Этап forward-only by the event's stage rank. New users also get a "Контакты" record; payments append to "Продажи Платформа". credits.* / subscription.expiring_soon events additionally post to the Slack alerts webhook. Idempotent on idempotency_key (24h dedup). Wire contract: LifecycleEventBuilderService in platform-api.
va-alerts
Единственный роут, которым пользуется команда снаружи. Поэтому у него свой секрет VA_WEBHOOK_SECRET, а не общий WEBHOOK_SECRET: общий открывает platform-events, то есть запись в наш NetHunt, и отдавать его наружу нельзя. Незаданный VA_WEBHOOK_SECRET закрывает роут (not_configured), а не открывает его — адрес живёт в чужом репозитории. Контракт для VA: docs/VA_ALERTS.md.
prod-alerts
Прод-алерты платформы → SHEV → Telegram. Секрет в теле ни Alertmanager, ни Sentry положить не умеют, поэтому у роута свои ключи и свой способ их предъявить:
- Alertmanager (Grafana-алерты идут через него):
Authorization: Bearer PROD_ALERTS_TOKEN(webhook_configs[].http_config.authorization); - Sentry (внутренняя интеграция, Alert Rule Action):
Sentry-Hook-Signature— HMAC-SHA256 сырого тела ключомSENTRY_ALERTS_CLIENT_SECRET. Хендлер видит сырое тело и заголовки черезctx.request(core).
Оба формата приводятся к одной форме (IProdAlert: fingerprint, status, severity, service, title, summary, link, count, startsAt/endsAt) и уходят пачкой в SHEV_ALERTS_URL с Authorization: Bearer SHEV_ALERTS_KEY. Повтор того же отпечатка+статуса в окне prod_alerts.collapse_minutes (10) не шлётся, а копится в count следующей отправки (KV). Алерты не из prod_alerts.environments отбрасываются. Из Sentry будят только event_alert/metric_alert; issue берётся лишь с action=resolved — закрыть инцидент. Всегда HTTP 200, итог в outcome (forwarded | collapsed | ignored | unauthorized | not_configured | invalid | forward_failed). Память схлопывания пишется только после успешной передачи: SHEV лежал — следующий алерт попробует снова.
shop-orders
Заказы мерча с shop.happ.tools → Slack. Публичный адрес: POST https://integ.happ.tools/happ/webhook/shop-orders (dev: integ.dev.happ.tools).
- Авторизация — подпись сырого тела, общий
WEBHOOK_SECRETне принимается:x-signature: sha256=<hex HMAC-SHA256(тело, SHOP_ORDERS_HMAC_SECRET)>, сравнение постоянное по времени.Content-Type: application/jsonобязателен — иначе core не отдаёт хендлеру сырое тело и подпись не сойдётся. - Незаданный (или плейсхолдер
CHANGE_ME_…)SHOP_ORDERS_HMAC_SECRETзакрывает роут. - Тело валидируется zod (
SHOP_ORDER_SCHEMAвutils/shop-order.util.ts): телефон+380XXXXXXXXX,carrier=nova_poshta,payment.method=cod,items≥ 1,qty≥ 1, деньги — целые гривны. - Сообщение уходит в
SLACK_SHOP_ORDERS_WEBHOOK_URL: шапкаNew order HS-10042 — 3 490 ₴ (COD), клиент, отделение НП, строки товаров, коммент, utm. - В отличие от остальных роутов happ отвечает настоящими статусами (через
withHttpStatusиз core):200 { ok: true },401подпись/секрет,400невалидный заказ (error— список полей),502Slack не принял или не настроен.
prod-logs
Чтение логов платформы для агента SHEV. Authorization: Bearer PROD_LOGS_TOKEN (свой ключ, общий WEBHOOK_SECRET не принимается). Сырого LogQL нет: тело — service (api | worker | client | admin | chat-widget | cms | temporal), contains, level, env, sinceMinutes (≤ 360), end, limit (≤ 200); селектор строит buildLogQuery по меткам promtail (env, service, level). Loki приватный, поэтому запрос идёт через datasource proxy Grafana: LOKI_URL = https://grafana.happ.tools/api/datasources/proxy/uid/Loki, LOKI_TOKEN — SA-токен Grafana (Viewer), CF_ACCESS_CLIENT_ID/SECRET — сервисный токен CF Access, который должен стоять в политике приложения grafana.happ.tools. Строки ужаты до 300 символов (Pino → msg · error · запрос · код) и замазаны. Нет ключа или доступа — not_configured; всегда HTTP 200, итог в outcome.
leads
source routes the lead folder: platform/instagram → Первая продажа, labs → Продажи Labs (others → default). Always creates a contact first, then the lead (Этап=Упала заявка, Источник=Сайт) linked back to it. Before creating, the funnel is searched by phone (+<digits>), then email: an existing card gets the lead as a comment (outcome: existing) instead of a duplicate. A failed lookup falls through to create. Free text comes in message or comment (createSalesLead).
Тот же source выбирает и канал в Slack: webinar → webinars-alerts (SLACK_WEBINAR_ALERTS_WEBHOOK_URL), всё остальное → sales-alerts. Пока вебинарный кред не задан, регистрации падают в sales-alerts, а не пропадают.
sales-demo-events
sales_demo.{requested,ready,failed,invite_sent,claimed} from platform-api (signed, via cf-proxy, webhook_secret = WEBHOOK_SECRET). Idempotent on idempotency_key (sd:evt:*). The card is found by KV sales-demo:ws:<workspaceId> → sales-demo:igsid:<igsid> → the Instagram column (only if sales_demo.instagramField is set), else a contact + «Воронка» lead is created (@username · демо <domain>, utm_page=sales-demo). Every event leaves a comment; invite_sent appends the email to «Email» (no overwrite) so the later user.signed_up lands on the same card; claimed moves «Этап» to sales_demo.claimedStage only from early stages (onboarding_discovery.earlyStages). requested and failed post to sales-alerts. Column keys instagramField / websiteField / demoLinkField / source default empty — empty means skip (NetHunt 400s on unknown fields and option values).
Config (D1 config, editable via integ-admin)
Folder ids and field-name maps for both flows live in src/config/defaults.ts (nethunt_folders, funnel_fields, sales_fields, stage_labels, alert_events, leads_folders, leads_fields, leads_static, sales_demo, …). NetHunt is keyed by display name, so the field maps are the RU column labels.
Credentials (D1 creds, via integ-admin or scripts/cli.ts creds:set happ <k> <v>)
NETHUNT_EMAIL, NETHUNT_API_KEY, WEBHOOK_SECRET, SLACK_SALES_ALERTS_WEBHOOK_URL, SLACK_WEBINAR_ALERTS_WEBHOOK_URL (webinars-alerts, optional), VA_WEBHOOK_SECRET + SLACK_VA_ALERTS_WEBHOOK_URL (va-alerts), PROD_ALERTS_TOKEN + SENTRY_ALERTS_CLIENT_SECRET + SHEV_ALERTS_URL + SHEV_ALERTS_KEY (prod-alerts), PROD_LOGS_TOKEN + LOKI_URL + LOKI_TOKEN + CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET (prod-logs).
Deploy
Trunk-based. Run POST /happ/setup once per env, then set creds. Same tag-promotion flow as every integ-core integration (release.yml → deploy-prod.yml).