Skip to content

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).

EndpointSourceDoes
/happ/webhook/platform-eventsplatform-api lifecycle eventsUpserts the funnel record (by email), records payments, Slack alerts
/happ/webhook/leadspublic marketing formCreates a contact, then a source-routed lead record
/happ/webhook/sales-demo-eventsplatform-api sales demo (IG)Find-or-create funnel card by igsid, comment per event, Slack
/happ/webhook/va-alertsexternal VA teamPosts an alert to Slack #va-alerts. No CRM write
/happ/webhook/prod-alertsAlertmanager/Grafana, SentryNormalizes, collapses repeats, forwards to SHEV POST /api/alerts
/happ/webhook/shop-ordersmerch store shop.happ.toolsValidates a signed order, posts it to Slack. No CRM write. Own HMAC
/happ/logs/querySHEV 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 — список полей), 502 Slack не принял или не настроен.

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).