dev #98

Merged
root merged 4 commits from dev into main 2026-06-04 12:45:44 +03:00
3 changed files with 95 additions and 3 deletions
Showing only changes of commit afaeec93d7 - Show all commits

View File

@@ -1,3 +1,4 @@
import { useFeatureFlag } from "@/hooks/use-feature-flag";
import { capture } from "@/lib/posthog";
import { Button } from "@sase/ui";
import { Link } from "@tanstack/react-router";
@@ -8,6 +9,21 @@ interface DemoFooterCtaProps {
source: string;
}
// A/B test `demo-cta-copy` (PostHog experiment). `control` = the original
// "sınırsız sorgulama" framing; `benefit` = a B2B value-led hook (find the
// right OEM part for a customer's vehicle in seconds). `undefined` (flag not
// yet loaded / user out of rollout) falls through to control.
const CTA_COPY = {
control: {
headline: "Sınırsız şase sorgulamak için ücretsiz hesap aç",
button: "Hesap Aç",
},
benefit: {
headline: "Müşteri aracının doğru OEM parçasını saniyede bul",
button: "Ücretsiz Dene",
},
} as const;
/**
* Footer conversion card on /demo pages — anti-gimmick B2B trust strip +
* single "Hesap Aç" primary button routed to /register (no VIN carried;
@@ -16,10 +32,14 @@ interface DemoFooterCtaProps {
* the headline; `mb-20` clears the Chatwoot widget in the bottom-right.
*/
export function DemoFooterCta({ source }: DemoFooterCtaProps) {
const flag = useFeatureFlag("demo-cta-copy");
const variant = flag === "benefit" ? "benefit" : "control";
const copy = CTA_COPY[variant];
return (
<div className="mb-20 mt-8 flex flex-col items-start gap-3 rounded-lg border border-border bg-muted/30 p-5 sm:mb-0 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-semibold">Sınırsız şase sorgulamak için ücretsiz hesap </p>
<p className="text-sm font-semibold">{copy.headline}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Kart bilgisi gerekmez · 30 gün ücretsiz · istediğin an iptal
</p>
@@ -29,8 +49,11 @@ export function DemoFooterCta({ source }: DemoFooterCtaProps) {
className="w-full shrink-0 sm:w-auto"
data-faro-user-action-name={`demo-footer-cta-${source}`}
>
<Link to="/register" onClick={() => capture("demo_to_register_click", { source })}>
Hesap
<Link
to="/register"
onClick={() => capture("demo_to_register_click", { source, cta_variant: variant })}
>
{copy.button}
<ArrowRight className="ml-1 h-4 w-4" />
</Link>
</Button>

View File

@@ -0,0 +1,27 @@
import { subscribeFeatureFlag } from "@/lib/posthog";
import { useEffect, useState } from "react";
/**
* Reactively read a PostHog feature flag in a component.
*
* Returns `undefined` until PostHog has loaded and evaluated flags (PostHog is
* lazy loaded), then the flag value — a `boolean` for simple flags or the
* variant key `string` for multivariant / experiment flags. The value updates
* automatically if flags reload (e.g. after login re-evaluates targeting).
*
* Always design the UI so `undefined` renders the safe/control branch — that's
* what shows during the brief load window and for users PostHog can't reach.
*
* @example
* const variant = useFeatureFlag("demo-cta-copy");
* const enabled = useFeatureFlag("new-billing-flow") === true;
*/
export function useFeatureFlag(key: string): string | boolean | undefined {
const [value, setValue] = useState<string | boolean | undefined>(undefined);
useEffect(() => {
return subscribeFeatureFlag(key, setValue);
}, [key]);
return value;
}

View File

@@ -32,7 +32,13 @@ export function initPostHog(): void {
person_profiles: "always",
capture_pageview: false,
capture_pageleave: false,
// Autocapture stays OFF: it serializes the text/attributes of clicked
// elements, which on the logged-in dashboard would ship customer VINs,
// OEM codes and PII to PostHog. Heatmaps below give us click/scroll/
// rage-click maps from coordinates only — the visual signal without the
// PII leak.
autocapture: false,
enable_heatmaps: true,
session_recording: {
maskAllInputs: false,
maskInputOptions: { password: true },
@@ -78,3 +84,39 @@ export function capturePageView(path: string): void {
export function setPeopleProperties(properties: Record<string, unknown>): void {
load().then((ph) => ph.people?.set(properties));
}
/**
* Subscribe to a feature flag's value, reactively.
*
* Fires `cb` once with the current value as soon as PostHog has loaded its
* flags, then again on every reload (e.g. after `identify()` re-evaluates
* targeting). Returns an unsubscribe function. Because PostHog itself is lazy
* loaded, the first callback is async — until then, treat the value as
* `undefined` (callers should default to the control behaviour).
*
* Works for both boolean flags (`true`/`false`) and multivariant /
* experiment flags (the variant key string, e.g. `"control"` / `"test"`).
* Reading a flag here also emits `$feature_flag_called`, which is what powers
* experiment exposure tracking — so just rendering a variant counts a user in.
*/
export function subscribeFeatureFlag(
key: string,
cb: (value: string | boolean | undefined) => void,
): () => void {
let unsub: (() => void) | undefined;
let cancelled = false;
load().then((ph) => {
if (cancelled) return;
cb(ph.getFeatureFlag(key));
unsub = ph.onFeatureFlags(() => cb(ph.getFeatureFlag(key)));
});
return () => {
cancelled = true;
unsub?.();
};
}
/** Read a feature flag's payload (the JSON attached to the matched variant). */
export function getFeatureFlagPayload(key: string): Promise<unknown> {
return load().then((ph) => ph.getFeatureFlagPayload(key));
}