From 651a115ab77cc94ef823cdb58326ebee387ff790 Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Tue, 3 Nov 2026 15:16:09 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F:=20utm=20=D1=81=20=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B3=D0=B0=20=D0=B8=D0=B7=20cookie=20sb=5Fu?= =?UTF-8?q?tm,=20=D0=B5=D1=81=D0=BB=D0=B8=20=D0=B2=20=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BE=D0=BA=20=D0=BD?= =?UTF-8?q?=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кнопки лендингов ведут на /register без меток, поэтому utmCampaign не был записан ни у одного пользователя. Caddy кладёт строку запроса с utm_* в cookie sb_utm на .second-brain.ru; страница регистрации берёт метки из адреса, а если их нет — из cookie. Значения: [A-Za-z0-9._-], до 100 символов. Co-Authored-By: Claude Opus 5.5 --- src/app/(auth)/register/page.tsx | 13 ++++++-- src/lib/__tests__/utm-cookie.test.ts | 40 +++++++++++++++++++++++ src/lib/utm-cookie.ts | 48 ++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/lib/__tests__/utm-cookie.test.ts create mode 100644 src/lib/utm-cookie.ts diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 8094de9..7d29fbb 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -1,6 +1,8 @@ import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; import { getSettings } from "@/lib/settings"; import { RegisterForm } from "./register-form"; +import { UTM_COOKIE, parseUtmCookie, pickUtm } from "@/lib/utm-cookie"; export default async function RegisterPage({ searchParams, @@ -15,6 +17,11 @@ export default async function RegisterPage({ const sp = await searchParams; const str = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v) ?? ""; + // Метки: из адреса, а если их нет — из cookie лендинга (src/lib/utm-cookie.ts). + const utm = pickUtm( + { utmSource: str(sp.utm_source), utmMedium: str(sp.utm_medium), utmCampaign: str(sp.utm_campaign) }, + parseUtmCookie((await cookies()).get(UTM_COOKIE)?.value), + ); return (
@@ -34,9 +41,9 @@ export default async function RegisterPage({ termsUrl={settings.termsUrl} offerUrl={settings.offerUrl} archetype={str(sp.archetype)} - utmSource={str(sp.utm_source)} - utmMedium={str(sp.utm_medium)} - utmCampaign={str(sp.utm_campaign)} + utmSource={utm.utmSource} + utmMedium={utm.utmMedium} + utmCampaign={utm.utmCampaign} gclid={str(sp.gclid)} />
diff --git a/src/lib/__tests__/utm-cookie.test.ts b/src/lib/__tests__/utm-cookie.test.ts new file mode 100644 index 0000000..b29a909 --- /dev/null +++ b/src/lib/__tests__/utm-cookie.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { cleanUtm, parseUtmCookie, pickUtm } from "../utm-cookie"; + +describe("utm-cookie", () => { + it("разбирает сырую строку запроса из cookie", () => { + expect(parseUtmCookie("utm_source=youtube&utm_medium=video&utm_campaign=test-20261103&fbclid=x")).toEqual({ + utmSource: "youtube", + utmMedium: "video", + utmCampaign: "test-20261103", + }); + }); + + it("понимает percent-кодирование", () => { + expect(parseUtmCookie("utm_campaign=a%2Db_c.d").utmCampaign).toBe("a-b_c.d"); + }); + + it("пустая и битая cookie — пустые метки", () => { + expect(parseUtmCookie(undefined)).toEqual({ utmSource: "", utmMedium: "", utmCampaign: "" }); + expect(parseUtmCookie("")).toEqual({ utmSource: "", utmMedium: "", utmCampaign: "" }); + expect(parseUtmCookie("%%%")).toEqual({ utmSource: "", utmMedium: "", utmCampaign: "" }); + }); + + it("отбрасывает значения не из белого списка символов и слишком длинные", () => { + expect(cleanUtm("ok-1")).toBe("ok-1"); + expect(cleanUtm("