From 5de2d12ad5bacaf85e2921362c132eefdb6bc879 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 12:07:53 +0300 Subject: [PATCH 1/2] feat(activation): A/B a guided one-click first decode (exp-activation-guided-decode) New users land on an empty search box with only a tiny fill-only "try example" link; activation (first vin_decode_success) sits at ~32%. This adds a prominent one-click "decode a sample vehicle" card that autoDecodes the sample VIN -> straight to the vehicle page (the aha-moment). Gated behind the exp-activation-guided-decode multivariate flag (control/guided), read ONLY for unactivated users (empty history) so power-user decodes don't dilute the metric; undefined -> control (status quo). PostHog experiment 83153 measures vin_decode_success. B2B-safe copy (sample vehicle, not "your car"). The experiment stays in draft until this ships to prod (web flags need the prod VITE_POSTHOG_KEY), then launch. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/messages/en.json | 3 ++ apps/web/src/messages/tr.json | 3 ++ .../__tests__/dashboard-search.test.tsx | 1 + apps/web/src/routes/dashboard/search.tsx | 45 ++++++++++++++++++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index f0f6c64..54b2877 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -696,6 +696,9 @@ "inputPlaceholder": "Enter VIN (17 characters)", "counter": "{count}/17 characters", "tryExample": "Try an example VIN →", + "activationCardTitle": "Try it now with a sample vehicle", + "activationCardHint": "No VIN to hand? See the parts catalog, diagrams and OEM codes in seconds with a sample vehicle.", + "activationCardCta": "Decode a sample vehicle", "submit": "Decode VIN", "errorTitle": "Couldn't decode VIN", "errorSubscriptionPrefix": "You have no active subscription. To access vehicle data,", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index 2358fff..0e5ad2b 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -696,6 +696,9 @@ "inputPlaceholder": "Şase numarasını girin (17 karakter)", "counter": "{count}/17 karakter", "tryExample": "Örnek şase deneyin →", + "activationCardTitle": "Örnek bir araçla hemen dene", + "activationCardHint": "Sorgulayacak şase elinde yoksa, örnek bir araçla parça kataloğunu, şemaları ve OEM kodlarını saniyeler içinde gör.", + "activationCardCta": "Örnek araçla sorgula", "submit": "Şase Çöz", "errorTitle": "Şase çözümlenemedi", "errorSubscriptionPrefix": "Aktif aboneliğiniz yok. Araç verilerine erişmek için", diff --git a/apps/web/src/routes/__tests__/dashboard-search.test.tsx b/apps/web/src/routes/__tests__/dashboard-search.test.tsx index 9a81371..c2adead 100644 --- a/apps/web/src/routes/__tests__/dashboard-search.test.tsx +++ b/apps/web/src/routes/__tests__/dashboard-search.test.tsx @@ -3,6 +3,7 @@ import { vi } from "vitest"; vi.mock("@/lib/posthog", () => ({ capture: vi.fn(), + subscribeFeatureFlag: vi.fn(() => () => {}), })); vi.mock("@/lib/api-client", () => ({ diff --git a/apps/web/src/routes/dashboard/search.tsx b/apps/web/src/routes/dashboard/search.tsx index 99d1c9f..6be6be4 100644 --- a/apps/web/src/routes/dashboard/search.tsx +++ b/apps/web/src/routes/dashboard/search.tsx @@ -5,7 +5,7 @@ import { ApiError, api } from "@/lib/api-client"; import { startAction } from "@/lib/faro"; import { useTranslation } from "@/lib/i18n"; import { KEYS_6, KEYS_17 } from "@/lib/keys"; -import { capture } from "@/lib/posthog"; +import { capture, subscribeFeatureFlag } from "@/lib/posthog"; import { toast } from "@/lib/toast"; import { Badge, Button, Input, Label, Separator } from "@sase/ui"; import { useQuery } from "@tanstack/react-query"; @@ -17,6 +17,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; const VIN_REGEX = /^[A-HJ-NPR-Z0-9]{17}$/; +// Sample VW Golf used by the "try example" affordance + the activation experiment. +const EXAMPLE_VIN = "WVWZZZ1JZ3W597935"; + interface VehicleHistoryItem { id: string; vin: string; @@ -160,6 +163,21 @@ function SearchPage() { queryFn: () => api.get("/vehicles/history?limit=6"), }); + // ─── Activation experiment: exp-activation-guided-decode ─────────────────── + // A prominent one-click "decode a sample vehicle" card vs the tiny fill-only + // "try example" link — does it lift the first-decode (activation) rate? + // Exposure is scoped to UNACTIVATED users (empty history): reading the flag only + // for them keeps power-user decodes out of the metric. undefined → control. + const isUnactivated = history !== undefined && history.length === 0; + const [activationVariant, setActivationVariant] = useState( + undefined, + ); + useEffect(() => { + if (!isUnactivated) return; + return subscribeFeatureFlag("exp-activation-guided-decode", setActivationVariant); + }, [isUnactivated]); + const showGuidedDecode = isUnactivated && activationVariant === "guided"; + // ─── Ctrl+K shortcut ─────────────────────────────────────────────────────── useEffect(() => { function handleKeyDown(e: KeyboardEvent) { @@ -478,7 +496,7 @@ function SearchPage() { } function fillExampleVin() { - setVin("WVWZZZ1JZ3W597935"); + setVin(EXAMPLE_VIN); inputRef.current?.focus(); } @@ -661,6 +679,29 @@ function SearchPage() { + {/* ─── Activation experiment (exp-activation-guided-decode): 1-click first decode ── */} + {showGuidedDecode && ( +
+
+

+ {t("search.activationCardTitle")} +

+

{t("search.activationCardHint")}

+
+ +
+ )} + {/* ─── SECTION 2: Live Preview Card ───────────────────────────────── */} {previewLoading && (
From 332574742885e39e8b6ab2d989c7fc6cc4d8bb30 Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Tue, 9 Jun 2026 12:29:37 +0300 Subject: [PATCH 2/2] feat(landing): 3-way CRO copy A/B/n on the hero (exp-landing-copy) Meta Ads traffic converts to signup well below expectation. Same design, three hero-copy variants (lossPill + title + subtitle) behind the exp-landing-copy multivariate flag, measuring user_signed_up (experiment 83160): - control: current "find the right part instantly" (feature / accuracy) - variant_a: "end wrong-part returns" (ROI / business-outcome + risk reversal) - variant_b: "find it in seconds, free, no card" (risk reversal / signup friction) useFeatureFlag on the index hero, undefined -> control. B2B-safe copy ("customer vehicle", not "your car"). Stays draft until prod, then launch. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/messages/en.json | 6 ++++++ apps/web/src/messages/tr.json | 6 ++++++ apps/web/src/routes/__tests__/index.test.tsx | 1 + apps/web/src/routes/index.tsx | 11 ++++++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 54b2877..5cd236d 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -792,6 +792,12 @@ "socialProofLabel": "500+ Businesses Trust Us", "title": "Find the Right Part Instantly", "subtitle": "Cross-referenced OEM codes from multiple catalogs, always up to date. Enter a VIN and ship the right part the first time.", + "lossPillA": "Wrong-part returns cost you thousands every month", + "titleA": "End Wrong-Part Returns for Good", + "subtitleA": "Enter the customer vehicle's VIN and find the correct OEM part the first time with cross-catalog verification. No returns, no losses — trusted by 500+ businesses. Try it free.", + "lossPillB": "Start looking up VINs free — no card required", + "titleB": "Find the Customer's Correct Part in Seconds", + "subtitleB": "Enter the 17-digit VIN and reach the right OEM codes, part diagrams and prices in seconds. 27 brands, unlimited lookups. Start free — no credit card.", "vinPlaceholder": "Example: WVWZZZ1JZ3W597935", "searchCta": "Search", "decoding": "Decoding...", diff --git a/apps/web/src/messages/tr.json b/apps/web/src/messages/tr.json index 0e5ad2b..5f5cb26 100644 --- a/apps/web/src/messages/tr.json +++ b/apps/web/src/messages/tr.json @@ -792,6 +792,12 @@ "socialProofLabel": "500+ İşletme Güveniyor", "title": "Doğru Parçayı Anında Bulun", "subtitle": "Birden fazla katalogda çapraz sorgulama ile her zaman en güncel OEM kodları. Şase numarasını girin, doğru parçayı ilk seferde gönderin.", + "lossPillA": "Yanlış parça iadeleri ayda binlerce TL kaybettiriyor", + "titleA": "Yanlış Parça İadelerine Son Verin", + "subtitleA": "Müşteri aracının şasesini girin; çapraz katalog doğrulamasıyla doğru OEM parçasını ilk seferde bulun. İade yok, kayıp yok — 500+ işletme güveniyor. Ücretsiz deneyin.", + "lossPillB": "Şase sorgulamaya ücretsiz başlayın — kart gerekmez", + "titleB": "Müşteri Aracının Doğru Parçasını Saniyede Bulun", + "subtitleB": "17 haneli şaseyi girin; doğru OEM kodlarına, parça şemalarına ve fiyatlara saniyeler içinde ulaşın. 27 marka, sınırsız sorgu. Ücretsiz başlayın, kredi kartı istemiyoruz.", "vinPlaceholder": "Örnek: WVWZZZ1JZ3W597935", "searchCta": "Ara", "decoding": "Çözülüyor...", diff --git a/apps/web/src/routes/__tests__/index.test.tsx b/apps/web/src/routes/__tests__/index.test.tsx index e9246a7..16158ea 100644 --- a/apps/web/src/routes/__tests__/index.test.tsx +++ b/apps/web/src/routes/__tests__/index.test.tsx @@ -12,6 +12,7 @@ import { vi } from "vitest"; // Mock the PostHog capture function vi.mock("@/lib/posthog", () => ({ capture: vi.fn(), + subscribeFeatureFlag: vi.fn(() => () => {}), })); // Mock the API client used for VIN decode diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index e0a087f..77a3c53 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -158,6 +158,7 @@ const RemotionEcommercePlayer = lazy(() => ), ); import { useAuth } from "@/hooks/use-auth"; +import { useFeatureFlag } from "@/hooks/use-feature-flag"; import { usePageMeta } from "@/hooks/use-page-meta"; import { ApiError, api } from "@/lib/api-client"; import { useTranslation } from "@/lib/i18n"; @@ -253,6 +254,10 @@ export const Route = createFileRoute("/")({ export function HomePage() { const { t } = useTranslation(); + // Landing-copy A/B/n (exp-landing-copy): SAME design, CRO copy variants for the + // hero pill/title/subtitle. undefined → control. Primary metric: user_signed_up. + const landingCopy = useFeatureFlag("exp-landing-copy"); + const copyV = landingCopy === "variant_a" ? "A" : landingCopy === "variant_b" ? "B" : ""; usePageMeta({ title: t("landing.meta.title"), @@ -887,7 +892,7 @@ export function HomePage() { - {t("landing.hero.lossPill")} + {t(`landing.hero.lossPill${copyV}`)}
{/* Social proof badge */} @@ -922,11 +927,11 @@ export function HomePage() {

- {t("landing.hero.title")} + {t(`landing.hero.title${copyV}`)}

- {t("landing.hero.subtitle")} + {t(`landing.hero.subtitle${copyV}`)}

{/* VIN Input */}