// Метки utm с лендингов (20261103, задание CMO «utm_campaign в LMS»). // // Лендинги второго мозга — поддомены second-brain.ru за общим Caddy на проде. // Caddy при заходе с utm_* в адресе кладёт сырую строку запроса в cookie // `sb_utm` на домен .second-brain.ru (см. фрагмент `(sb_utm)` в prod.caddy). // Кнопка лендинга ведёт на /register без меток, поэтому страница регистрации // берёт их из адреса, а если там пусто — из этой cookie. export const UTM_COOKIE = "sb_utm"; export type Utm = { utmSource: string; utmMedium: string; utmCampaign: string }; const EMPTY: Utm = { utmSource: "", utmMedium: "", utmCampaign: "" }; // Значение метки: только латиница, цифры, точка, дефис, подчёркивание; до 100 символов. // Остальное (кириллица, пробелы, кавычки) отбрасываем целиком — в отчёт идут слаги. export function cleanUtm(v: string | null | undefined): string { if (!v) return ""; const s = v.trim().slice(0, 100); return /^[A-Za-z0-9._-]+$/.test(s) ? s : ""; } export function parseUtmCookie(raw: string | null | undefined): Utm { if (!raw) return EMPTY; let q: URLSearchParams; try { q = new URLSearchParams(raw.length > 2000 ? raw.slice(0, 2000) : raw); } catch { return EMPTY; } return { utmSource: cleanUtm(q.get("utm_source")), utmMedium: cleanUtm(q.get("utm_medium")), utmCampaign: cleanUtm(q.get("utm_campaign")), }; } // Метки из адреса важнее cookie: прямой переход на /register?utm_… — самый свежий. // Берём набор целиком из одного источника, не смешиваем поля. export function pickUtm(fromQuery: Utm, fromCookie: Utm): Utm { const q = { utmSource: cleanUtm(fromQuery.utmSource), utmMedium: cleanUtm(fromQuery.utmMedium), utmCampaign: cleanUtm(fromQuery.utmCampaign), }; if (q.utmSource || q.utmMedium || q.utmCampaign) return q; return fromCookie; }