feat(FN-297): add social proof impression tracking to landing page testimonials (+2 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- docs(FN-297): update FN-275 audit with social proof event locations
- fix(FN-297): format IntersectionObserver mock in test file
- feat(FN-297): add social proof impression tracking to landing page testimonials

Files changed:
apps/web/src/routes/__tests__/index.test.tsx |   7 ++
 apps/web/src/routes/index.tsx                | 111 +++++++++++++++++++++++++--
 docs/product/funnel-audit-p1-cro-2026-05.md  |  19 ++++-
 3 files changed, 127 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-297
This commit is contained in:
Fusion
2026-05-13 07:28:25 +00:00
parent 80596a4ceb
commit 3dd4dc2f77
3 changed files with 127 additions and 10 deletions

View File

@@ -71,6 +71,13 @@ import { useAuthStore } from "@/stores/auth.store";
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"IntersectionObserver",
vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
})),
);
});
test("captures vin_decode_success on successful decode", async () => {

View File

@@ -25,7 +25,7 @@ import {
X,
Zap,
} from "lucide-react";
import { Suspense, lazy, useEffect, useState } from "react";
import { Suspense, lazy, useEffect, useRef, useState } from "react";
// Remotion — lazy loaded to keep initial bundle lean
const RemotionSchemaPlayer = lazy(() =>
@@ -553,6 +553,69 @@ export function HomePage() {
setVin("WVWZZZ1JZ3W597935");
};
// ─── Social proof impression tracking ────────────────────────────────────
const heroBadgeRef = useRef<HTMLDivElement>(null);
const testimonialStripRef = useRef<HTMLDivElement>(null);
const testimonialsRef = useRef<HTMLElement>(null);
const heroBadgeFiredRef = useRef(false);
const testimonialStripFiredRef = useRef(false);
const testimonialsFiredRef = useRef(false);
useEffect(() => {
const el = heroBadgeRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !heroBadgeFiredRef.current) {
heroBadgeFiredRef.current = true;
capture("social_proof_impression", { page: "landing", section: "hero_badge" });
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
const el = testimonialStripRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !testimonialStripFiredRef.current) {
testimonialStripFiredRef.current = true;
capture("social_proof_impression", { page: "landing", section: "testimonial_strip" });
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
const el = testimonialsRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !testimonialsFiredRef.current) {
testimonialsFiredRef.current = true;
capture("social_proof_impression", { page: "landing", section: "testimonials" });
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
function handleSocialProofEngaged(section: string) {
capture("social_proof_engaged", { page: "landing", section });
}
return (
<div className="min-h-screen bg-background text-foreground">
{/* ─── 1. STICKY HEADER ─────────────────────────────────────────── */}
@@ -708,7 +771,20 @@ export function HomePage() {
</div>
{/* Social proof badge */}
<div className="mt-3 inline-flex items-center gap-3 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground">
<div
ref={heroBadgeRef}
// biome-ignore lint/a11y/useSemanticElements: analytics-only interaction on content block
role="button"
tabIndex={0}
onClick={() => handleSocialProofEngaged("hero_badge")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSocialProofEngaged("hero_badge");
}
}}
className="mt-3 inline-flex cursor-pointer items-center gap-3 rounded-full border border-border bg-muted px-4 py-1.5 text-xs text-muted-foreground"
>
<div className="flex -space-x-2">
{["AY", "MK", "ÖD", "BT"].map((initials) => (
<div
@@ -889,7 +965,10 @@ export function HomePage() {
</section>
{/* ─── 3.5. TESTIMONIAL STRIP ────────────────────────────────── */}
<section className="border-b border-border bg-background px-4 py-14 sm:px-6">
<section
ref={testimonialStripRef}
className="border-b border-border bg-background px-4 py-14 sm:px-6"
>
<div className="mx-auto max-w-7xl">
<div className="text-center">
<p className="text-sm text-muted-foreground">Kullanıcılarımız ne diyor?</p>
@@ -915,7 +994,17 @@ export function HomePage() {
].map((t) => (
<div
key={t.name}
className="rounded-2xl border border-border bg-surface px-6 py-5 transition-all duration-300 hover:-translate-y-0.5 hover:border-foreground/10"
// biome-ignore lint/a11y/useSemanticElements: analytics-only interaction on content block
role="button"
tabIndex={0}
onClick={() => handleSocialProofEngaged("testimonial_strip")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSocialProofEngaged("testimonial_strip");
}
}}
className="cursor-pointer rounded-2xl border border-border bg-surface px-6 py-5 transition-all duration-300 hover:-translate-y-0.5 hover:border-foreground/10"
>
<div className="flex gap-0.5">
{dynamicKeys(5).map((k) => (
@@ -1358,7 +1447,7 @@ export function HomePage() {
</section>
{/* ─── 9.6. TESTIMONIALS ────────────────────────────────────── */}
<section className="bg-surface-alt px-4 py-20 sm:px-6">
<section ref={testimonialsRef} className="bg-surface-alt px-4 py-20 sm:px-6">
<div className="mx-auto max-w-7xl">
<div className="text-center">
<SectionBadge>Müşteri Yorumları</SectionBadge>
@@ -1371,7 +1460,17 @@ export function HomePage() {
{TESTIMONIALS.map((t) => (
<div
key={t.name}
className="group rounded-2xl border border-border bg-surface p-6 transition-all duration-300 hover:-translate-y-1 hover:border-foreground/20 hover:shadow-[var(--shadow-md)]"
// biome-ignore lint/a11y/useSemanticElements: analytics-only interaction on content block
role="button"
tabIndex={0}
onClick={() => handleSocialProofEngaged("testimonials")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSocialProofEngaged("testimonials");
}
}}
className="group cursor-pointer rounded-2xl border border-border bg-surface p-6 transition-all duration-300 hover:-translate-y-1 hover:border-foreground/20 hover:shadow-[var(--shadow-md)]"
>
<div className="flex gap-0.5">
{dynamicKeys(t.rating).map((k) => (

View File

@@ -26,8 +26,8 @@ The baseline funnel — `plan_selected → checkout_started → payment_initiate
| FN-209 | `trial_banner_viewed` | ❌ Missing | Banner renders but sends no event |
| FN-209 | `trial_banner_dismissed` | ❌ Missing | Dismiss tracked only via localStorage |
| FN-209 | `trial_banner_converted` | ❌ Missing | CTA link navigates, no conversion event |
| FN-210 | `social_proof_impression` | ❌ Missing | Testimonial is static HTML, no visibility tracking |
| FN-210 | `social_proof_engagement` | ❌ Missing | No interaction tracking on social proof elements |
| FN-210 | `social_proof_impression` | ✅ Instrumented (FN-282, FN-297) | IntersectionObserver on landing/auth/subscription social proof |
| FN-210 | `social_proof_engagement` | ✅ Instrumented (FN-282, FN-297) | Click/keyboard interaction tracking on social proof elements |
| Baseline | `payment_success` | ❌ Missing | Gap documented in FN-101; never instrumented |
### 1.2 Existing Funnel Events (Available)
@@ -218,7 +218,16 @@ See the Fusion board for:
| `downgrade_offer_declined` | `apps/web/src/routes/dashboard/subscription/index.tsx` | 702 |
| `receipt_uploaded` | `apps/web/src/components/payment/payment-content.tsx` | 189 |
| Trial banner (no events) | `apps/web/src/components/trial-urgency-banner.tsx` | 23 |
| Social proof (no events) | `apps/web/src/routes/_auth.tsx` | 103 |
| `social_proof_impression` (hero badge) | `apps/web/src/routes/index.tsx` | 573 |
| `social_proof_impression` (testimonial strip) | `apps/web/src/routes/index.tsx` | 590 |
| `social_proof_impression` (testimonials) | `apps/web/src/routes/index.tsx` | 607 |
| `social_proof_engaged` (landing) | `apps/web/src/routes/index.tsx` | 616 |
| `social_proof_impression` (auth) | `apps/web/src/routes/_auth.tsx` | 23 |
| `social_proof_engaged` (auth) | `apps/web/src/routes/_auth.tsx` | 33 |
| `social_proof_impression` (subscription trust) | `apps/web/src/routes/dashboard/subscription/index.tsx` | 409 |
| `social_proof_impression` (subscription payment) | `apps/web/src/routes/dashboard/subscription/index.tsx` | 426 |
| `social_proof_engaged` (subscription trust) | `apps/web/src/routes/dashboard/subscription/index.tsx` | 436 |
| `social_proof_engaged` (subscription payment) | `apps/web/src/routes/dashboard/subscription/index.tsx` | 440 |
## Appendix B: Files Requiring Instrumentation
@@ -226,6 +235,8 @@ See the Fusion board for:
|------|--------------|
| `apps/web/src/components/trial-urgency-banner.tsx` | `trial_banner_viewed`, `trial_banner_dismissed`, `trial_banner_converted` |
| `apps/web/src/routes/dashboard/subscription/index.tsx` | `cancel_flow_viewed`, `cancel_save_clicked` (cancel dialog section) |
| `apps/web/src/routes/_auth.tsx` | `social_proof_impression` (IntersectionObserver on testimonial) |
| `apps/web/src/routes/_auth.tsx` | `social_proof_impression` (IntersectionObserver) → ✅ Done (FN-282) |
| `apps/web/src/routes/index.tsx` | `social_proof_impression`, `social_proof_engaged` (landing page social proof) → ✅ Done (FN-297) |
| `apps/web/src/routes/dashboard/subscription/index.tsx` | `social_proof_impression`, `social_proof_engaged` (trust badges, payment) → ✅ Done (FN-282) |
| `apps/api/src/payments/payments.controller.ts` | `payment_success`, `payment_failed` (Iyzico callback + EFT approval) |
| `apps/web/src/components/payment/payment-content.tsx` | `payment_success` (on Iyzico redirect return or EFT confirmation) |