-
+
i'm not an absolute beginner, so how does subscribing
to opensox.ai make sense to me?
@@ -485,7 +515,10 @@ const Pitch = () => {
-
+
will the quality of your service reduce as you grow?
@@ -496,7 +529,10 @@ const Pitch = () => {
-
+
how does opensox.ai pro help me?
@@ -555,7 +591,10 @@ const Pitch = () => {
-
+
how much time does it take to get the results?
@@ -572,7 +611,10 @@ const Pitch = () => {
-
+
why should i trust you?
@@ -599,7 +641,10 @@ const Pitch = () => {
-
+
are there any alternatives to what you provide?
@@ -611,7 +656,10 @@ const Pitch = () => {
-
+
what's the difference between opensox pro and a
course?
@@ -624,7 +672,10 @@ const Pitch = () => {
-
+
is it for an absolute beginner?
@@ -633,7 +684,10 @@ const Pitch = () => {
-
+
in what cases shouldn't i invest in opensox pro?
@@ -649,7 +703,7 @@ const Pitch = () => {
- you don't wanna do it fast
+ you don't wanna do it fast
@@ -663,7 +717,10 @@ const Pitch = () => {
-
+
are you the best in the market?
@@ -715,6 +772,7 @@ const Pitch = () => {
buttonText="Invest"
buttonClassName="w-full max-w-md"
callbackUrl={callbackUrl}
+ buttonLocation="pitch_page"
/>
) : (
diff --git a/apps/web/src/app/(main)/(landing)/pricing/page.tsx b/apps/web/src/app/(main)/(landing)/pricing/page.tsx
index ac4feb70..2a246bbd 100644
--- a/apps/web/src/app/(main)/(landing)/pricing/page.tsx
+++ b/apps/web/src/app/(main)/(landing)/pricing/page.tsx
@@ -428,6 +428,7 @@ const SecondaryPricingCard = ({ callbackUrl }: { callbackUrl: string }) => {
planIdOk ? "" : "opacity-60 cursor-not-allowed"
}`}
callbackUrl={callbackUrl}
+ buttonLocation="pricing_page"
/>
+
{children}
diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx
index 2a7f0629..4b6bcc54 100644
--- a/apps/web/src/app/providers.tsx
+++ b/apps/web/src/app/providers.tsx
@@ -2,10 +2,98 @@
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
+import { useSession } from "next-auth/react";
import PostHogPageView from "./PostHogPageView";
+// Session storage key to track if sign-in was initiated
+const SIGN_IN_INITIATED_KEY = "posthog_sign_in_initiated";
+const SIGN_IN_PROVIDER_KEY = "posthog_sign_in_provider";
+
+/**
+ * PostHog Auth Tracker
+ *
+ * This component must be rendered INSIDE SessionProvider.
+ * It tracks sign_in_completed events when user authenticates.
+ */
+export function PostHogAuthTracker() {
+ const { data: session, status } = useSession();
+ const hasTrackedSignIn = useRef(false);
+ const previousStatus = useRef
(null);
+
+ useEffect(() => {
+ if (status === "loading") return;
+
+ try {
+ // Check if PostHog is initialized
+ if (!posthog.__loaded) return;
+
+ // Detect transition from unauthenticated to authenticated
+ const wasSignInInitiated =
+ sessionStorage.getItem(SIGN_IN_INITIATED_KEY) === "true";
+ const storedProvider = sessionStorage.getItem(SIGN_IN_PROVIDER_KEY) as
+ | "google"
+ | "github"
+ | null;
+
+ if (status === "authenticated" && session?.user) {
+ // Check if this is a fresh sign-in (not just a page refresh)
+ const isNewSignIn =
+ wasSignInInitiated ||
+ (previousStatus.current === "unauthenticated" &&
+ !hasTrackedSignIn.current);
+
+ if (isNewSignIn && !hasTrackedSignIn.current) {
+ hasTrackedSignIn.current = true;
+
+ // Determine provider from stored value
+ const provider = storedProvider || "google"; // Default to google if unknown
+
+ // Track sign-in completed EVENT only (no person properties)
+ posthog.capture("sign_in_completed", {
+ provider: provider,
+ is_new_user: false,
+ });
+
+ if (process.env.NODE_ENV === "development") {
+ console.log("[Analytics] Event tracked: sign_in_completed", {
+ provider,
+ is_new_user: false,
+ });
+ }
+
+ // Clear the sign-in tracking flags
+ sessionStorage.removeItem(SIGN_IN_INITIATED_KEY);
+ sessionStorage.removeItem(SIGN_IN_PROVIDER_KEY);
+ }
+ } else if (status === "unauthenticated") {
+ // Reset tracking flag for next sign-in
+ hasTrackedSignIn.current = false;
+
+ if (process.env.NODE_ENV === "development") {
+ console.log("[PostHog] User unauthenticated");
+ }
+ }
+
+ // Track previous status
+ previousStatus.current = status;
+ } catch (error) {
+ if (process.env.NODE_ENV === "development") {
+ console.error("[PostHog] Error handling auth state:", error);
+ }
+ }
+ }, [session, status]);
+
+ return null;
+}
+
+/**
+ * PostHog Provider
+ * NOTE: This provider does NOT handle auth tracking.
+ * Use PostHogAuthTracker inside SessionProvider for that.
+ */
export function PostHogProvider({ children }: { children: React.ReactNode }) {
+ // Initialize PostHog
useEffect(() => {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
@@ -13,11 +101,37 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
if (posthogKey && posthogHost) {
posthog.init(posthogKey, {
api_host: posthogHost,
+
+ // Pageview tracking
capture_pageview: false, // Disable automatic pageview capture, as we capture manually
- capture_pageleave: true,
+ capture_pageleave: true, // Track when users leave pages
+
+ // Privacy settings
+ disable_session_recording: true, // Privacy: No session recordings
+ respect_dnt: true, // Respect Do Not Track header
+ ip: false, // Do not store IP addresses (anonymize)
+
+ // Persistence settings
+ persistence: "localStorage+cookie", // Persist anonymous ID across sessions
+
+ // Performance settings
+ autocapture: false, // We use custom events for better control
+
+ // Development settings
+ loaded: (posthog) => {
+ if (process.env.NODE_ENV === "development") {
+ console.log("[PostHog] Initialized successfully");
+ // Enable debug mode in development
+ posthog.debug(false); // Set to true to see all PostHog logs
+ }
+ },
});
} else {
- console.error("PostHog key or host is not defined");
+ if (process.env.NODE_ENV === "development") {
+ console.warn(
+ "[PostHog] Key or host is not defined - analytics disabled"
+ );
+ }
}
}, []);
diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx
index afea0818..22265205 100644
--- a/apps/web/src/components/dashboard/Sidebar.tsx
+++ b/apps/web/src/components/dashboard/Sidebar.tsx
@@ -20,7 +20,7 @@ import {
Squares2X2Icon,
ChevronDownIcon,
LockClosedIcon,
- AcademicCapIcon
+ AcademicCapIcon,
} from "@heroicons/react/24/outline";
import { useShowSidebar } from "@/store/useShowSidebar";
import { signOut, useSession } from "next-auth/react";
@@ -28,6 +28,7 @@ import { ProfilePic } from "./ProfilePic";
import { useSubscription } from "@/hooks/useSubscription";
import { OpensoxProBadge } from "../sheet/OpensoxProBadge";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { useAnalytics } from "@/hooks/useAnalytics";
type RouteConfig = {
path: string;
@@ -82,6 +83,7 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
const pathname = usePathname();
const { isPaidUser } = useSubscription();
const [proSectionExpanded, setProSectionExpanded] = useState(true);
+ const { trackLinkClick, trackButtonClick } = useAnalytics();
// auto-expand pro section if user is on a premium route
useEffect(() => {
@@ -96,6 +98,13 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
}, [pathname, isPaidUser]);
const reqFeatureHandler = () => {
+ // Track feature request click
+ trackLinkClick(
+ "https://github.com/apsinghdev/opensox/issues",
+ "Request a feature",
+ "sidebar",
+ true
+ );
window.open("https://github.com/apsinghdev/opensox/issues", "_blank");
};
@@ -103,6 +112,8 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
if (isPaidUser) {
setProSectionExpanded(!proSectionExpanded);
} else {
+ // Track upgrade button click for free users
+ trackButtonClick("Opensox Pro", "sidebar");
router.push("/pricing");
}
};
@@ -166,7 +177,14 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
const isActive =
pathname === route.path || pathname.startsWith(`${route.path}/`);
return (
-
+ {
+ // Track navigation link click
+ trackLinkClick(route.path, route.label, "sidebar", false);
+ }}
+ >
+
{
+ // Track premium navigation link click
+ trackLinkClick(
+ route.path,
+ route.label,
+ "sidebar",
+ false
+ );
+ }}
+ >
(
router.push("/pricing")}
+ onClick={() => {
+ // Track locked premium feature click
+ trackButtonClick(`${route.label} (Locked)`, "sidebar");
+ router.push("/pricing");
+ }}
className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 opacity-50 hover:opacity-75 group"
role="button"
tabIndex={0}
@@ -357,6 +391,7 @@ export default function Sidebar({ overlay = false }: { overlay?: boolean }) {
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
+ trackButtonClick(`${route.label} (Locked)`, "sidebar");
router.push("/pricing");
}
}}
@@ -427,6 +462,7 @@ function ProfileMenu({ isCollapsed }: { isCollapsed: boolean }) {
const [open, setOpen] = useState(false);
const { data: session } = useSession();
const router = useRouter();
+ const { trackButtonClick, trackLinkClick } = useAnalytics();
const isLoggedIn = !!session;
const fullName = session?.user?.name || "User";
@@ -506,6 +542,13 @@ function ProfileMenu({ isCollapsed }: { isCollapsed: boolean }) {
{isLoggedIn && (
{
+ // Track account settings click
+ trackLinkClick(
+ "/dashboard/account",
+ "Account Settings",
+ "sidebar",
+ false
+ );
router.push("/dashboard/account");
setOpen(false);
}}
@@ -518,6 +561,8 @@ function ProfileMenu({ isCollapsed }: { isCollapsed: boolean }) {
{isLoggedIn ? (
{
+ // Track logout click
+ trackButtonClick("Logout", "sidebar");
signOut({ callbackUrl: "/" });
setOpen(false);
}}
@@ -529,6 +574,8 @@ function ProfileMenu({ isCollapsed }: { isCollapsed: boolean }) {
) : (
{
+ // Track login click
+ trackButtonClick("Login", "sidebar");
router.push("/login");
setOpen(false);
}}
diff --git a/apps/web/src/components/landing-sections/CTA.tsx b/apps/web/src/components/landing-sections/CTA.tsx
index 67f8fe3e..bdd282b0 100644
--- a/apps/web/src/components/landing-sections/CTA.tsx
+++ b/apps/web/src/components/landing-sections/CTA.tsx
@@ -1,31 +1,48 @@
-import { Terminal } from 'lucide-react'
-import React from 'react'
-import PrimaryButtom from '../ui/custom-button'
-import Image from 'next/image'
-import Link from 'next/link'
+"use client";
+
+import { Terminal } from "lucide-react";
+import React from "react";
+import PrimaryButtom from "../ui/custom-button";
+import Image from "next/image";
+import Link from "next/link";
+import { useAnalytics } from "@/hooks/useAnalytics";
const CTA = () => {
- return (
-
-
-
-
Ready to dive into Open Source?
-
Join 10,000+ engineers accelerating in open-source.
-
-
-
-
- Get Started
-
-
-
- )
-}
+ const { trackButtonClick } = useAnalytics();
+
+ const handleGetStartedClick = () => {
+ trackButtonClick("Get Started", "cta_section");
+ };
+
+ return (
+
+
+
+
+ Ready to dive into Open Source?
+
+
+ Join 10,000+ engineers accelerating in open-source.
+
+
+
+
+
+ Get Started
+
+
+
+ );
+};
-export default CTA
\ No newline at end of file
+export default CTA;
diff --git a/apps/web/src/components/landing-sections/Hero.tsx b/apps/web/src/components/landing-sections/Hero.tsx
index cb90c84a..8c452100 100644
--- a/apps/web/src/components/landing-sections/Hero.tsx
+++ b/apps/web/src/components/landing-sections/Hero.tsx
@@ -5,8 +5,11 @@ import React from "react";
import PrimaryButtom from "../ui/custom-button";
import Link from "next/link";
import { motion } from "framer-motion";
+import { useAnalytics } from "@/hooks/useAnalytics";
const Hero = () => {
+ const { trackButtonClick } = useAnalytics();
+
// Container variants for staggered children animation
const containerVariants = {
hidden: { opacity: 0 },
@@ -32,6 +35,10 @@ const Hero = () => {
},
};
+ const handleGetStartedClick = () => {
+ trackButtonClick("Get Started", "hero");
+ };
+
return (
{
}}
className="cursor-pointer z-30 [will-change:transform,opacity] motion-reduce:transition-none motion-reduce:transform-none"
>
-
+
Get Started
diff --git a/apps/web/src/components/landing-sections/footer.tsx b/apps/web/src/components/landing-sections/footer.tsx
index 4b6b061b..464ba77a 100644
--- a/apps/web/src/components/landing-sections/footer.tsx
+++ b/apps/web/src/components/landing-sections/footer.tsx
@@ -3,15 +3,23 @@ import React from "react";
import { Twitter, Email, Discord, Youtube, Github } from "../icons/icons";
import Link from "next/link";
import Image from "next/image";
+import { useAnalytics } from "@/hooks/useAnalytics";
const Footer = () => {
+ const { trackLinkClick } = useAnalytics();
+
const handleEmailClick = () => {
+ trackLinkClick("mailto:hi@opensox.ai", "Email", "footer", true);
const emailSubject = encodeURIComponent("[Inquiry about Opensox AI]");
const emailBody = encodeURIComponent("Heyyo,\n\nwanna chat?");
const mailtoLink = `mailto:hi@opensox.ai?subject=${emailSubject}&body=${emailBody}`;
window.open(mailtoLink, "_blank");
};
+ const handleSocialClick = (url: string, name: string) => {
+ trackLinkClick(url, name, "footer", true);
+ };
+
return (
);
-}
+};
+
+export default ProSessionsPage;