From 2be3239855076640997e8e15654e355a4c3cf9e5 Mon Sep 17 00:00:00 2001 From: Aman Raj <113578582+huamanraj@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:49:27 +0530 Subject: [PATCH 01/13] feat: posthog event tracting for analytics --- .../src/app/(main)/(landing)/pitch/page.tsx | 104 ++++-- .../src/app/(main)/(landing)/pricing/page.tsx | 1 + apps/web/src/app/layout.tsx | 3 +- apps/web/src/app/providers.tsx | 120 +++++- apps/web/src/components/dashboard/Sidebar.tsx | 55 ++- .../src/components/landing-sections/CTA.tsx | 73 ++-- .../src/components/landing-sections/Hero.tsx | 13 +- .../components/landing-sections/footer.tsx | 29 ++ .../components/landing-sections/navbar.tsx | 27 +- apps/web/src/components/login/SignInPage.tsx | 18 +- .../src/components/payment/PaymentFlow.tsx | 41 +++ apps/web/src/hooks/useAnalytics.ts | 259 +++++++++++++ apps/web/src/lib/analytics.ts | 348 ++++++++++++++++++ 13 files changed, 1027 insertions(+), 64 deletions(-) create mode 100644 apps/web/src/hooks/useAnalytics.ts create mode 100644 apps/web/src/lib/analytics.ts diff --git a/apps/web/src/app/(main)/(landing)/pitch/page.tsx b/apps/web/src/app/(main)/(landing)/pitch/page.tsx index 21b82b72..032949c5 100644 --- a/apps/web/src/app/(main)/(landing)/pitch/page.tsx +++ b/apps/web/src/app/(main)/(landing)/pitch/page.tsx @@ -106,7 +106,10 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-4" > -

+

mission statement

@@ -134,7 +137,10 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-6" > -

+

my goal

@@ -190,7 +196,10 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-6" > -

+

the plan

@@ -258,13 +267,19 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-8" > -

+

philosophies i follow

{/* Philosophy #1 */}
-

+

#1 stay small. stay effective.

@@ -303,9 +318,9 @@ const Pitch = () => {

- if i go with this approach, i'll have to sacrifice those - fancy dreams of raising millions, being on the front - page of magazines, having millions of users, etc. + if i go with this approach, i'll have to sacrifice + those fancy dreams of raising millions, being on the + front page of magazines, having millions of users, etc.



but the good part is i'll be able to stay genuine @@ -341,7 +356,10 @@ const Pitch = () => { {/* Philosophy #2 */}

-

+

#2 go beyond what you promise.

@@ -374,7 +392,10 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-6" > -

+

so how small?

@@ -425,7 +446,10 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-6" > -

+

what existing investors said about me?

@@ -458,13 +482,19 @@ const Pitch = () => { }} className="max-w-4xl mx-auto space-y-8" > -

+

questions you may have

-

+

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 && ( - {error &&

    {error}

    }
    )} + {error && ( +

    {error}

    + )}
    ); diff --git a/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx new file mode 100644 index 00000000..b891b846 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useSubscription } from "@/hooks/useSubscription"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { proSessions, type ProSession } from "@/data/pro-sessions"; +import { + Play, + CheckCircle2, + ExternalLink, + ArrowLeft, +} from "lucide-react"; +import Link from "next/link"; + +function SessionCard({ + session, + index, +}: { + session: ProSession; + index: number; +}) { + const [isHovered, setIsHovered] = useState(false); + + const handleClick = () => { + window.open(session.youtubeUrl, "_blank", "noopener,noreferrer"); + }; + + return ( +
    setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + className="group relative bg-dash-surface border border-dash-border rounded-xl p-5 cursor-pointer + transition-all duration-300 ease-out + hover:border-brand-purple/50 hover:bg-dash-hover hover:shadow-lg hover:shadow-brand-purple/5 + hover:-translate-y-1 active:scale-[0.98]" + style={{ + animationDelay: `${index * 50}ms`, + }} + > + {/* Session number badge */} +
    +
    +
    + + {String(session.id).padStart(2, "0")} + +
    +

    + {session.title} +

    +
    +
    + +
    +
    + + {/* Topics covered */} +
    +

    + Topics Covered +

    +
      + {session.topicsCovered.map((topic, topicIndex) => ( +
    • + + + {topic} + +
    • + ))} +
    +
    + + {/* Watch now indicator */} +
    + + Watch on YouTube +
    + + {/* Hover glow effect */} +
    +
    + ); +} + +export default function ProSessionsPage() { + const { isPaidUser, isLoading } = useSubscription(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && !isPaidUser) { + router.push("/pricing"); + } + }, [isPaidUser, isLoading, router]); + + if (isLoading) { + return ( +
    +
    +
    +

    Loading sessions...

    +
    +
    + ); + } + + if (!isPaidUser) { + return null; + } + + return ( +
    +
    + {/* Header */} +
    + {/* Back link */} + + + Back to Pro Dashboard + + +
    + +

    + Opensox Pro Sessions +

    +
    +

    + Recordings of Opensox Pro session meetings covering advanced open source strategies, + real-world examples, and insider tips to accelerate your journey. +

    +
    + + {/* Sessions Grid */} +
    + {proSessions.map((session, index) => ( + + ))} +
    + + {/* Footer note */} +
    +

    + More sessions coming soon • Stay tuned for updates +

    +
    +
    +
    + ); +} diff --git a/apps/web/src/data/pro-sessions.ts b/apps/web/src/data/pro-sessions.ts new file mode 100644 index 00000000..dacd0422 --- /dev/null +++ b/apps/web/src/data/pro-sessions.ts @@ -0,0 +1,112 @@ +/** + * Pro Sessions Data + * + * Contains all the Opensox Pro Session YouTube videos with their topics + */ + +export interface ProSession { + id: number; + title: string; + youtubeUrl: string; + topicsCovered: string[]; + duration?: string; +} + +export const proSessions: ProSession[] = [ + { + id: 1, + title: "Pro Session 01", + youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ", + topicsCovered: [ + "Introduction to Open Source Contributions", + "Setting up your development environment", + "Understanding Git workflow basics", + ], + }, + { + id: 2, + title: "Pro Session 02", + youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI", + topicsCovered: [ + "Finding your first issue", + "Reading project documentation effectively", + "Communicating with maintainers", + ], + }, + { + id: 3, + title: "Pro Session 03", + youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ", + topicsCovered: [ + "Writing clean pull requests", + "Code review best practices", + "Handling feedback gracefully", + ], + }, + { + id: 4, + title: "Pro Session 04", + youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI", + topicsCovered: [ + "Building your Open Source portfolio", + "Showcasing contributions on GitHub", + "Networking in the OSS community", + ], + }, + { + id: 5, + title: "Pro Session 05", + youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ", + topicsCovered: [ + "Advanced Git techniques", + "Rebasing and resolving conflicts", + "Cherry-picking commits", + ], + }, + { + id: 6, + title: "Pro Session 06", + youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI", + topicsCovered: [ + "Understanding CI/CD pipelines", + "Writing effective tests", + "Debugging failed builds", + ], + }, + { + id: 7, + title: "Pro Session 07", + youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ", + topicsCovered: [ + "Contributing to documentation", + "Writing technical content", + "Documentation as code", + ], + }, + { + id: 8, + title: "Pro Session 08", + youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI", + topicsCovered: [ + "Preparing for GSoC", + "Writing winning proposals", + "Building relationships with mentors", + ], + }, + { + id: 9, + title: "Pro Session 09", + youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ", + topicsCovered: [ + "Landing your first OSS internship", + "Resume tips for developers", + "Leveraging OSS for career growth", + "Landing your first OSS internship", + "Resume tips for developers", + "Leveraging OSS for career growth", + "Landing your first OSS internship", + "Resume tips for developers", + "Leveraging OSS for career growth", + ], + }, +]; From b82770355851db2de591fffa97fe0be515ed35db Mon Sep 17 00:00:00 2001 From: Aman Raj <113578582+huamanraj@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:41:47 +0530 Subject: [PATCH 12/13] fix: made divs keyboard accessible --- .../(main)/dashboard/pro/sessions/page.tsx | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx index b891b846..945394d1 100644 --- a/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx +++ b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx @@ -1,16 +1,13 @@ "use client"; -import { useSubscription } from "@/hooks/useSubscription"; -import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; -import { proSessions, type ProSession } from "@/data/pro-sessions"; -import { - Play, - CheckCircle2, - ExternalLink, - ArrowLeft, -} from "lucide-react"; + +import { ArrowLeft, CheckCircle2, ExternalLink, Play } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; + +import { useSubscription } from "@/hooks/useSubscription"; +import { proSessions, type ProSession } from "@/data/pro-sessions"; function SessionCard({ session, @@ -27,13 +24,23 @@ function SessionCard({ return (
    { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleClick(); + } + }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} + aria-label={`Watch session: ${session.title}`} className="group relative bg-dash-surface border border-dash-border rounded-xl p-5 cursor-pointer transition-all duration-300 ease-out hover:border-brand-purple/50 hover:bg-dash-hover hover:shadow-lg hover:shadow-brand-purple/5 - hover:-translate-y-1 active:scale-[0.98]" + hover:-translate-y-1 active:scale-[0.98] + focus-visible:ring-2 focus-visible:ring-brand-purple/50 focus-visible:outline-none" style={{ animationDelay: `${index * 50}ms`, }} @@ -155,14 +162,14 @@ export default function ProSessionsPage() {
    -

    Opensox Pro Sessions

    - Recordings of Opensox Pro session meetings covering advanced open source strategies, - real-world examples, and insider tips to accelerate your journey. + Recordings of Opensox Pro session meetings covering advanced open + source strategies, real-world examples, and insider tips to + accelerate your journey.

    From e5c4ab81ea26bbbea20c3957467425c012d03452 Mon Sep 17 00:00:00 2001 From: Aman Raj <113578582+huamanraj@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:55:35 +0530 Subject: [PATCH 13/13] fix: ts errors --- .../src/app/(main)/dashboard/pro/sessions/page.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx index 945394d1..337fa857 100644 --- a/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx +++ b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx @@ -9,13 +9,13 @@ import { useRouter } from "next/navigation"; import { useSubscription } from "@/hooks/useSubscription"; import { proSessions, type ProSession } from "@/data/pro-sessions"; -function SessionCard({ +const SessionCard = ({ session, index, }: { session: ProSession; index: number; -}) { +}): JSX.Element | null => { const [isHovered, setIsHovered] = useState(false); const handleClick = () => { @@ -114,14 +114,14 @@ function SessionCard({ transition-opacity duration-500 pointer-events-none" style={{ background: - "radial-gradient(ellipse at center, rgba(85, 25, 247, 0.05) 0%, transparent 70%)", + "radial-gradient(ellipse at center, rgb(85 25 247 / 0.05) 0%, transparent 70%)", }} />
    ); -} +}; -export default function ProSessionsPage() { +const ProSessionsPage = (): JSX.Element | null => { const { isPaidUser, isLoading } = useSubscription(); const router = useRouter(); @@ -189,4 +189,6 @@ export default function ProSessionsPage() {
    ); -} +}; + +export default ProSessionsPage;