Files
sase.tr/apps/web/src/routes/__root.tsx
Semih Yesilyurt fa6937bb5f
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
fix(analytics): stop orphaning anonymous person on load, breaking signup attribution
__root identify effect called posthog.reset() whenever `user` was falsy —
which includes the brief window while the session resolves on every page
load. After the signup hard-redirect this rotated the anonymous distinct_id,
orphaning the pre-signup anonymous person that carries `user_signed_up` and
first-touch `$initial_utm_*`. The later identify() then merged a fresh, empty
anon id, so signups never linked to trial/payment (only ~16% stitched) and
channel attribution read "(none)" for 100% of signups.

- __root.tsx: gate the effect on isLoading and only reset() on a real
  identified -> anonymous transition (logout), tracked via a ref. Logout
  still resets via dashboard handleSignOut.
- register.tsx: identify() within the still-active anonymous session before
  firing user_signed_up, so the anon->identified merge carries $initial_utm_*
  and attributes the signup (email flow; Google is handled on OAuth return).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:54:08 +03:00

163 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { InAppEscape } from "@/components/in-app-escape";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { resetChatwootUser, setChatwootUser } from "@/lib/chatwoot";
import { useTranslation } from "@/lib/i18n";
import { trackPageView as trackMetaPageView } from "@/lib/meta-pixel";
import { capturePageView, identifyUser, resetUser } from "@/lib/posthog";
import { Toaster } from "@/lib/toast";
import { getUserSettings } from "@/lib/user-settings";
import { Button } from "@sase/ui";
import type { QueryClient } from "@tanstack/react-query";
import { Link, Outlet, createRootRouteWithContext, useLocation } from "@tanstack/react-router";
import { ArrowLeft, Home, Search } from "lucide-react";
import { useEffect, useRef } from "react";
interface RouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
notFoundComponent: NotFoundComponent,
});
function NotFoundComponent() {
return (
<main className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6 py-12">
{/* Ambient brand glow */}
<div className="pointer-events-none absolute -left-32 top-1/4 h-[500px] w-[500px] rounded-full bg-brand/8 blur-[140px]" />
<div className="pointer-events-none absolute -right-32 bottom-1/4 h-[400px] w-[400px] rounded-full bg-brand/5 blur-[120px]" />
<div className="relative max-w-xl text-center">
<p className="font-mono text-sm font-medium uppercase tracking-[0.2em] text-muted-foreground">
404 sayfa bulunamadı
</p>
<h1 className="mt-6 font-[family-name:var(--font-display)] text-6xl font-bold tracking-tight sm:text-7xl">
Yanlış parça,
<br />
<span className="text-muted-foreground">yanlış adres.</span>
</h1>
<p className="mx-auto mt-6 max-w-md text-base text-muted-foreground">
Aradığın sayfa silinmiş ya da hiç olmamış olabilir. Aşağıdan ana sayfaya dönebilir veya
doğrudan şase aramaya gidebilirsin.
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link to="/">
<Button variant="outline" className="rounded-full">
<ArrowLeft className="size-4" />
Ana sayfaya dön
</Button>
</Link>
<Link to="/dashboard/search">
<Button variant="brand" className="rounded-full">
<Search className="size-4" />
Şase aramaya git
</Button>
</Link>
</div>
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-muted-foreground">
<Link
to="/"
className="inline-flex items-center gap-1.5 transition-colors hover:text-foreground"
>
<Home className="size-3.5" />
Anasayfa
</Link>
<span className="size-1 rounded-full bg-border" aria-hidden="true" />
<Link to="/pricing" className="transition-colors hover:text-foreground">
Fiyatlandırma
</Link>
<span className="size-1 rounded-full bg-border" aria-hidden="true" />
<Link to="/demo" className="transition-colors hover:text-foreground">
Demo
</Link>
<span className="size-1 rounded-full bg-border" aria-hidden="true" />
<Link to="/contact" className="transition-colors hover:text-foreground">
İletişim
</Link>
</div>
</div>
</main>
);
}
function applyTheme(theme: "light" | "dark" | "system") {
const isDark =
theme === "dark" ||
(theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", isDark);
}
function RootComponent() {
const location = useLocation();
const { user, isLoading } = useAuth();
const { t } = useTranslation();
// Pageview tracking
useEffect(() => {
capturePageView(location.pathname);
trackMetaPageView();
}, [location.pathname]);
// User identification.
//
// CRITICAL: never call posthog.reset() while the session is still resolving,
// nor for first-time anonymous visitors. reset() rotates the anonymous
// distinct_id; firing it on every load (user is briefly null while the
// session loads) orphaned the pre-signup anonymous person — the one that
// carries `user_signed_up` and the first-touch `$initial_utm_*`. The later
// identify() then merged a fresh, empty anon id instead, so signups never
// linked to trial/payment and channel attribution read "(none)" for everyone.
// Only reset on a genuine identified -> anonymous transition (logout);
// explicit logout also resets via dashboard.tsx's handleSignOut.
const wasIdentifiedRef = useRef(false);
useEffect(() => {
if (isLoading) return; // session still resolving — leave identity untouched
if (user) {
wasIdentifiedRef.current = true;
identifyUser({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
});
// Verified Chatwoot identity: fetch the server-computed HMAC, then set the
// widget user. On failure the widget stays anonymous (chat still works).
api
.get<{ identifier: string; identifierHash: string }>("/chatwoot/identity")
.then((res) => setChatwootUser(user, res.identifierHash))
.catch(() => {});
} else if (wasIdentifiedRef.current) {
wasIdentifiedRef.current = false;
resetUser();
resetChatwootUser();
}
}, [user, isLoading]);
useEffect(() => {
const theme = getUserSettings().theme ?? "dark";
applyTheme(theme);
if (theme === "system") {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => applyTheme("system");
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}
}, []);
return (
<>
<a href="#main-content" className="skip-link">
{t("a11y.skipToContent")}
</a>
<InAppEscape />
<Outlet />
<Toaster position="top-center" />
</>
);
}