diff --git a/apps/web/next.config.js b/apps/web/next.config.js index a8a9c9ce7..ea9f0f120 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -22,6 +22,8 @@ const nextConfig = { "@rallly/icons", "@rallly/ui", "@rallly/tailwind-config", + "@rallly/posthog", + "@rallly/emails", ], webpack(config) { config.module.rules.push({ diff --git a/apps/web/package.json b/apps/web/package.json index ad9590ccb..27a59cae3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,6 +31,7 @@ "@rallly/emails": "*", "@rallly/icons": "*", "@rallly/languages": "*", + "@rallly/posthog": "*", "@rallly/tailwind-config": "*", "@rallly/ui": "*", "@sentry/nextjs": "*", @@ -70,8 +71,6 @@ "next-i18next": "^13.0.3", "php-serialize": "^4.1.1", "postcss": "^8.4.31", - "posthog-js": "^1.154.0", - "posthog-node": "^4.0.1", "react-big-calendar": "^1.8.1", "react-hook-form": "^7.42.1", "react-hook-form-persist": "^3.0.0", diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx index 99f0e2cd1..e28062cce 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/delete-account-dialog.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import type { DialogProps} from "@rallly/ui/dialog"; @@ -19,7 +20,6 @@ import { useForm } from "react-hook-form"; import { Trans } from "@/components/trans"; import { useTranslation } from "@/i18n/client"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; export function DeleteAccountDialog({ email, diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx index 0906f5315..33dcc15b5 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/profile-picture.tsx @@ -1,3 +1,4 @@ +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { useToast } from "@rallly/ui/hooks/use-toast"; import * as Sentry from "@sentry/nextjs"; @@ -10,7 +11,6 @@ import { useUser } from "@/components/user-provider"; import { IfCloudHosted } from "@/contexts/environment"; import { useTranslation } from "@/i18n/client"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; const allowedMimeTypes = z.enum(["image/jpeg", "image/png"]); diff --git a/apps/web/src/app/[locale]/(admin)/sidebar.tsx b/apps/web/src/app/[locale]/(admin)/sidebar.tsx index 6efceeae6..e3ccec394 100644 --- a/apps/web/src/app/[locale]/(admin)/sidebar.tsx +++ b/apps/web/src/app/[locale]/(admin)/sidebar.tsx @@ -1,5 +1,6 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { cn } from "@rallly/ui"; import { Button } from "@rallly/ui/button"; import { DialogTrigger } from "@rallly/ui/dialog"; @@ -26,7 +27,6 @@ import { Trans } from "@/components/trans"; import { IfGuest, useUser } from "@/components/user-provider"; import { IfFreeUser } from "@/contexts/plan"; import type { IconComponent } from "@/types"; -import { usePostHog } from "@/utils/posthog"; function NavItem({ href, diff --git a/apps/web/src/app/[locale]/(auth)/login/login-form.tsx b/apps/web/src/app/[locale]/(auth)/login/login-form.tsx index d4a2adcf9..e713cb3ea 100644 --- a/apps/web/src/app/[locale]/(auth)/login/login-form.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/login-form.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert"; import { Button } from "@rallly/ui/button"; import { Input } from "@rallly/ui/input"; @@ -16,7 +17,6 @@ import { VerifyCode, verifyCode } from "@/components/auth/auth-forms"; import { Spinner } from "@/components/spinner"; import { isSelfHosted } from "@/utils/constants"; import { validEmail } from "@/utils/form-validation"; -import { usePostHog } from "@/utils/posthog"; const allowGuestAccess = !isSelfHosted; diff --git a/apps/web/src/app/[locale]/(auth)/register/register-page.tsx b/apps/web/src/app/[locale]/(auth)/register/register-page.tsx index 829123fbc..75a523f32 100644 --- a/apps/web/src/app/[locale]/(auth)/register/register-page.tsx +++ b/apps/web/src/app/[locale]/(auth)/register/register-page.tsx @@ -1,5 +1,6 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { Form, @@ -15,7 +16,6 @@ import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; import { signIn } from "next-auth/react"; import { useTranslation } from "next-i18next"; -import { usePostHog } from "posthog-js/react"; import React from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; diff --git a/apps/web/src/app/[locale]/auth/login/login-page.tsx b/apps/web/src/app/[locale]/auth/login/login-page.tsx index 30a75a8b6..d0070d99f 100644 --- a/apps/web/src/app/[locale]/auth/login/login-page.tsx +++ b/apps/web/src/app/[locale]/auth/login/login-page.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { useMutation } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; @@ -9,7 +10,6 @@ import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; import { Skeleton } from "@/components/skeleton"; import { Trans } from "@/components/trans"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; type PageProps = { magicLink: string; email: string }; diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx index 1886ecbc6..bae2c6a25 100644 --- a/apps/web/src/app/[locale]/layout.tsx +++ b/apps/web/src/app/[locale]/layout.tsx @@ -8,6 +8,8 @@ import React from "react"; import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector"; import { Providers } from "@/app/providers"; +import { getServerSession } from "@/auth"; +import { SessionProvider } from "@/auth/session-provider"; const inter = Inter({ subsets: ["latin"], @@ -21,21 +23,24 @@ export const viewport: Viewport = { userScalable: false, }; -export default function Root({ +export default async function Root({ children, params: { locale }, }: { children: React.ReactNode; params: { locale: string }; }) { + const session = await getServerSession(); return ( - - {children} - - + + + {children} + + + ); diff --git a/apps/web/src/app/[locale]/poll/[urlId]/duplicate-dialog.tsx b/apps/web/src/app/[locale]/poll/[urlId]/duplicate-dialog.tsx index 49f76644e..36957e243 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/duplicate-dialog.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/duplicate-dialog.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import type { DialogProps} from "@rallly/ui/dialog"; @@ -16,7 +17,6 @@ import { useRouter } from "next/navigation"; import { DuplicateForm } from "@/app/[locale]/poll/[urlId]/duplicate-form"; import { trpc } from "@/app/providers"; import { Trans } from "@/components/trans"; -import { usePostHog } from "@/utils/posthog"; const formName = "duplicate-form"; export function DuplicateDialog({ diff --git a/apps/web/src/app/[locale]/timezone-change-detector.tsx b/apps/web/src/app/[locale]/timezone-change-detector.tsx index b74eb04d5..f36cd2733 100644 --- a/apps/web/src/app/[locale]/timezone-change-detector.tsx +++ b/apps/web/src/app/[locale]/timezone-change-detector.tsx @@ -1,5 +1,6 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { Dialog, @@ -8,7 +9,6 @@ import { DialogHeader, DialogTitle, } from "@rallly/ui/dialog"; -import { usePostHog } from "posthog-js/react"; import React, { useState } from "react"; import { Trans } from "@/components/trans"; diff --git a/apps/web/src/app/components/logout-button.tsx b/apps/web/src/app/components/logout-button.tsx index f08fc32cf..c707b882b 100644 --- a/apps/web/src/app/components/logout-button.tsx +++ b/apps/web/src/app/components/logout-button.tsx @@ -1,9 +1,8 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import type { ButtonProps } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button"; -import { usePostHog } from "@/utils/posthog"; - export function LogoutButton({ children, onClick, diff --git a/apps/web/src/app/guest.ts b/apps/web/src/app/guest.ts index 041737c10..147762526 100644 --- a/apps/web/src/app/guest.ts +++ b/apps/web/src/app/guest.ts @@ -4,7 +4,7 @@ import { randomid } from "@rallly/utils/nanoid"; import languageParser from "accept-language-parser"; import type { NextRequest, NextResponse } from "next/server"; import type { JWT } from "next-auth/jwt"; -import { encode } from "next-auth/jwt"; +import { decode, encode } from "next-auth/jwt"; const supportedLocales = Object.keys(languages); @@ -61,10 +61,20 @@ export async function resetUser(req: NextRequest, res: NextResponse) { export async function initGuest(req: NextRequest, res: NextResponse) { const { name } = getCookieSettings(); - - if (req.cookies.has(name)) { - // already has a session token - return; + const token = req.cookies.get(name)?.value; + if (token) { + try { + const jwt = await decode({ + token, + secret: process.env.SECRET_PASSWORD, + }); + if (jwt) { + return jwt; + } + } catch (error) { + // invalid token + console.error(error); + } } const locale = await getLocaleFromHeader(req); diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index 4536b00e6..6c5fb3fea 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -1,9 +1,9 @@ "use client"; +import { PostHogProvider } from "@rallly/posthog/client"; import { TooltipProvider } from "@rallly/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createTRPCReact } from "@trpc/react-query"; import { domMax, LazyMotion } from "framer-motion"; -import { SessionProvider } from "next-auth/react"; import { useState } from "react"; import { UserProvider } from "@/components/user-provider"; @@ -32,13 +32,13 @@ export function Providers(props: { children: React.ReactNode }) { - + {props.children} - + diff --git a/apps/web/src/auth.ts b/apps/web/src/auth.ts index 518192c0f..0ee18d735 100644 --- a/apps/web/src/auth.ts +++ b/apps/web/src/auth.ts @@ -1,4 +1,5 @@ import { prisma } from "@rallly/database"; +import { posthog } from "@rallly/posthog/server"; import { absoluteUrl } from "@rallly/utils/absolute-url"; import { generateOtp, randomid } from "@rallly/utils/nanoid"; import type { @@ -16,15 +17,15 @@ import EmailProvider from "next-auth/providers/email"; import GoogleProvider from "next-auth/providers/google"; import type { Provider } from "next-auth/providers/index"; -import { posthog } from "@/app/posthog"; -import { CustomPrismaAdapter } from "@/auth/custom-prisma-adapter"; -import { mergeGuestsIntoUser } from "@/auth/merge-user"; import { env } from "@/env"; import type { RegistrationTokenPayload } from "@/trpc/types"; import { getEmailClient } from "@/utils/emails"; import { getValueByPath } from "@/utils/get-value-by-path"; import { decryptToken } from "@/utils/session"; +import { CustomPrismaAdapter } from "./auth/custom-prisma-adapter"; +import { mergeGuestsIntoUser } from "./auth/merge-user"; + const providers: Provider[] = [ // When a user registers, we don't want to go through the email verification process // so this provider allows us exchange the registration token for a session token diff --git a/apps/web/src/auth/session-provider.tsx b/apps/web/src/auth/session-provider.tsx new file mode 100644 index 000000000..194ebe444 --- /dev/null +++ b/apps/web/src/auth/session-provider.tsx @@ -0,0 +1,8 @@ +"use client"; + +import type { SessionProviderProps } from "next-auth/react"; +import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"; + +export function SessionProvider(props: SessionProviderProps) { + return ; +} diff --git a/apps/web/src/components/create-poll.tsx b/apps/web/src/components/create-poll.tsx index 4218397c6..909417273 100644 --- a/apps/web/src/components/create-poll.tsx +++ b/apps/web/src/components/create-poll.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { Card, @@ -19,7 +20,6 @@ import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; import { trpc } from "@/trpc/client"; import { setCookie } from "@/utils/cookies"; -import { usePostHog } from "@/utils/posthog"; import type { NewEventData} from "./forms"; import { PollDetailsForm, PollOptionsForm } from "./forms"; diff --git a/apps/web/src/components/discussion/discussion.tsx b/apps/web/src/components/discussion/discussion.tsx index 70361fd18..931650e6f 100644 --- a/apps/web/src/components/discussion/discussion.tsx +++ b/apps/web/src/components/discussion/discussion.tsx @@ -1,4 +1,5 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import { cn } from "@rallly/ui"; import { Badge } from "@rallly/ui/badge"; import { Button } from "@rallly/ui/button"; @@ -37,7 +38,6 @@ import { usePermissions } from "@/contexts/permissions"; import { usePoll } from "@/contexts/poll"; import { useRole } from "@/contexts/role"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; import { requiredString } from "../../utils/form-validation"; import TruncatedLinkify from "../poll/truncated-linkify"; diff --git a/apps/web/src/components/forms/poll-settings.tsx b/apps/web/src/components/forms/poll-settings.tsx index 65547d2c3..2330c0f08 100644 --- a/apps/web/src/components/forms/poll-settings.tsx +++ b/apps/web/src/components/forms/poll-settings.tsx @@ -1,3 +1,4 @@ +import { usePostHog } from "@rallly/posthog/client"; import { cn } from "@rallly/ui"; import { Card, @@ -17,7 +18,6 @@ import { Trans } from "react-i18next"; import { PayWallDialog } from "@/components/pay-wall-dialog"; import { ProFeatureBadge } from "@/components/pro-feature-badge"; import { usePlan } from "@/contexts/plan"; -import { usePostHog } from "@/utils/posthog"; export type PollSettingsFormData = { requireParticipantEmail: boolean; diff --git a/apps/web/src/components/participant-dropdown.tsx b/apps/web/src/components/participant-dropdown.tsx index 2065b9a41..e7b3ffe13 100644 --- a/apps/web/src/components/participant-dropdown.tsx +++ b/apps/web/src/components/participant-dropdown.tsx @@ -1,4 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod"; +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { Dialog, @@ -40,7 +41,6 @@ import { useDeleteParticipantMutation } from "@/components/poll/mutations"; import { Trans } from "@/components/trans"; import { trpc } from "@/trpc/client"; import { useFormValidation } from "@/utils/form-validation"; -import { usePostHog } from "@/utils/posthog"; export const ParticipantDropdown = ({ participant, diff --git a/apps/web/src/components/poll/manage-poll.tsx b/apps/web/src/components/poll/manage-poll.tsx index 56fe19505..95f359c66 100644 --- a/apps/web/src/components/poll/manage-poll.tsx +++ b/apps/web/src/components/poll/manage-poll.tsx @@ -1,3 +1,4 @@ +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { useDialog } from "@rallly/ui/dialog"; import { @@ -34,7 +35,6 @@ import { ProFeatureBadge } from "@/components/pro-feature-badge"; import { Trans } from "@/components/trans"; import { usePlan } from "@/contexts/plan"; import { usePoll } from "@/contexts/poll"; -import { usePostHog } from "@/utils/posthog"; import { DeletePollDialog } from "./manage-poll/delete-poll-dialog"; import { useCsvExporter } from "./manage-poll/use-csv-exporter"; diff --git a/apps/web/src/components/poll/manage-poll/delete-poll-dialog.tsx b/apps/web/src/components/poll/manage-poll/delete-poll-dialog.tsx index 9c1711cf9..3c2867728 100644 --- a/apps/web/src/components/poll/manage-poll/delete-poll-dialog.tsx +++ b/apps/web/src/components/poll/manage-poll/delete-poll-dialog.tsx @@ -1,3 +1,4 @@ +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { Dialog, @@ -11,7 +12,6 @@ import * as React from "react"; import { Trans } from "@/components/trans"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; export const DeletePollDialog: React.FunctionComponent<{ open: boolean; diff --git a/apps/web/src/components/poll/mutations.ts b/apps/web/src/components/poll/mutations.ts index a0bc74f5d..74f0c5c10 100644 --- a/apps/web/src/components/poll/mutations.ts +++ b/apps/web/src/components/poll/mutations.ts @@ -1,6 +1,7 @@ +import { usePostHog } from "@rallly/posthog/client"; + import { usePoll } from "@/components/poll-context"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; import type { ParticipantForm } from "./types"; diff --git a/apps/web/src/components/poll/notifications-toggle.tsx b/apps/web/src/components/poll/notifications-toggle.tsx index 5ff2c82f1..29fcfd336 100644 --- a/apps/web/src/components/poll/notifications-toggle.tsx +++ b/apps/web/src/components/poll/notifications-toggle.tsx @@ -1,3 +1,4 @@ +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import { Icon } from "@rallly/ui/icon"; import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip"; @@ -10,7 +11,6 @@ import { Skeleton } from "@/components/skeleton"; import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; import { trpc } from "@/trpc/client"; -import { usePostHog } from "@/utils/posthog"; import { usePoll } from "../poll-context"; diff --git a/apps/web/src/components/upgrade-button.tsx b/apps/web/src/components/upgrade-button.tsx index c2217fddc..b46b41a08 100644 --- a/apps/web/src/components/upgrade-button.tsx +++ b/apps/web/src/components/upgrade-button.tsx @@ -1,10 +1,9 @@ +import { usePostHog } from "@rallly/posthog/client"; import { Button } from "@rallly/ui/button"; import Link from "next/link"; import { Trans } from "next-i18next"; import React from "react"; -import { usePostHog } from "@/utils/posthog"; - export const UpgradeButton = ({ children, annual, diff --git a/apps/web/src/components/user-provider.tsx b/apps/web/src/components/user-provider.tsx index bc58e7aa4..a0144779d 100644 --- a/apps/web/src/components/user-provider.tsx +++ b/apps/web/src/components/user-provider.tsx @@ -1,11 +1,11 @@ "use client"; +import { usePostHog } from "@rallly/posthog/client"; import type { Session } from "next-auth"; import { useSession } from "next-auth/react"; import React from "react"; import { Spinner } from "@/components/spinner"; import { useSubscription } from "@/contexts/plan"; -import { PostHogProvider } from "@/contexts/posthog"; import { PreferencesProvider } from "@/contexts/preferences"; import { useTranslation } from "@/i18n/client"; import { trpc } from "@/trpc/client"; @@ -60,6 +60,25 @@ export const UserProvider = (props: { children?: React.ReactNode }) => { const updatePreferences = trpc.user.updatePreferences.useMutation(); const { t, i18n } = useTranslation(); + const posthog = usePostHog(); + + const isGuest = !user?.email; + const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby"; + + React.useEffect(() => { + if (user) { + posthog?.identify(user.id, { + email: user.email, + name: user.name, + tier, + timeZone: user.timeZone ?? null, + image: user.image ?? null, + locale: user.locale ?? i18n.language, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user?.id]); + if (!user) { return (
@@ -68,9 +87,6 @@ export const UserProvider = (props: { children?: React.ReactNode }) => { ); } - const isGuest = !user.email; - const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby"; - return ( { await session.update(newPreferences); }} > - {props.children} + {props.children} ); diff --git a/apps/web/src/contexts/posthog.tsx b/apps/web/src/contexts/posthog.tsx deleted file mode 100644 index 7a9fbaf1e..000000000 --- a/apps/web/src/contexts/posthog.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; -import { usePathname, useSearchParams } from "next/navigation"; -import posthog from "posthog-js"; -import { PostHogProvider as Provider, usePostHog } from "posthog-js/react"; -import React from "react"; -import { useMount } from "react-use"; - -import { useUser } from "@/components/user-provider"; -import { env } from "@/env"; - -type PostHogProviderProps = React.PropsWithChildren; - -if (typeof window !== "undefined" && env.NEXT_PUBLIC_POSTHOG_API_KEY) { - posthog.init(env.NEXT_PUBLIC_POSTHOG_API_KEY, { - debug: false, - api_host: env.NEXT_PUBLIC_POSTHOG_API_HOST, - capture_pageview: false, - capture_pageleave: true, - disable_session_recording: true, - enable_heatmaps: false, - persistence: "memory", - autocapture: false, - opt_out_capturing_by_default: false, - }); -} - -function usePostHogPageView() { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const posthog = usePostHog(); - const previousUrl = React.useRef(null); - React.useEffect(() => { - // Track pageviews - if (pathname && posthog) { - let url = window.origin + pathname; - if (searchParams?.toString()) { - url = url + `?${searchParams.toString()}`; - } - if (previousUrl.current !== url) { - posthog.capture("$pageview", { - $current_url: url, - }); - previousUrl.current = url; - } - } - }, [pathname, searchParams, posthog]); -} - -export function PostHogProvider(props: PostHogProviderProps) { - const { user } = useUser(); - - usePostHogPageView(); - - useMount(() => { - if (user.email) { - posthog.identify(user.id, { - email: user.email, - name: user.name, - tier: user.tier, - timeZone: user.timeZone, - locale: user.locale, - }); - } - }); - - return {props.children}; -} diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 23e50cdb6..9c2af3fe9 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,4 +1,5 @@ import languages from "@rallly/languages"; +import { withPostHog } from "@rallly/posthog/next/middleware"; import { NextResponse } from "next/server"; import withAuth from "next-auth/middleware"; @@ -34,7 +35,11 @@ export const middleware = withAuth( const res = NextResponse.rewrite(newUrl); - await initGuest(req, res); + const jwt = await initGuest(req, res); + + if (jwt?.sub) { + await withPostHog(res, { distinctID: jwt.sub }); + } return res; }, diff --git a/apps/web/src/pages/api/auth/[...nextauth].ts b/apps/web/src/pages/api/auth/[...nextauth].ts index 09e252547..5350908b6 100644 --- a/apps/web/src/pages/api/auth/[...nextauth].ts +++ b/apps/web/src/pages/api/auth/[...nextauth].ts @@ -1,6 +1,6 @@ +import { posthogApiHandler } from "@rallly/posthog/server"; import type { NextApiRequest, NextApiResponse } from "next"; -import { posthogApiHandler } from "@/app/posthog"; import { AuthApiRoute } from "@/auth"; import { composeApiHandlers } from "@/utils/next"; diff --git a/apps/web/src/pages/api/stripe/webhook.ts b/apps/web/src/pages/api/stripe/webhook.ts index 028d6ad0d..ea3842047 100644 --- a/apps/web/src/pages/api/stripe/webhook.ts +++ b/apps/web/src/pages/api/stripe/webhook.ts @@ -1,12 +1,12 @@ import type { Stripe } from "@rallly/billing"; import { stripe } from "@rallly/billing"; import { prisma } from "@rallly/database"; +import { posthog, posthogApiHandler } from "@rallly/posthog/server"; import * as Sentry from "@sentry/node"; import { buffer } from "micro"; import type { NextApiRequest, NextApiResponse } from "next"; import { z } from "zod"; -import { posthog, posthogApiHandler } from "@/app/posthog"; import { composeApiHandlers } from "@/utils/next"; export const config = { diff --git a/apps/web/src/pages/api/trpc/[trpc].ts b/apps/web/src/pages/api/trpc/[trpc].ts index e57956aa7..fe3e4383a 100644 --- a/apps/web/src/pages/api/trpc/[trpc].ts +++ b/apps/web/src/pages/api/trpc/[trpc].ts @@ -1,10 +1,10 @@ +import { posthogApiHandler } from "@rallly/posthog/server"; import * as Sentry from "@sentry/nextjs"; import { TRPCError } from "@trpc/server"; import { createNextApiHandler } from "@trpc/server/adapters/next"; -import { posthogApiHandler } from "@/app/posthog"; import { getServerSession } from "@/auth"; -import type { AppRouter} from "@/trpc/routers"; +import type { AppRouter } from "@/trpc/routers"; import { appRouter } from "@/trpc/routers"; import { getEmailClient } from "@/utils/emails"; import { composeApiHandlers } from "@/utils/next"; diff --git a/apps/web/src/trpc/routers/auth.ts b/apps/web/src/trpc/routers/auth.ts index 463bac65f..f119f7f13 100644 --- a/apps/web/src/trpc/routers/auth.ts +++ b/apps/web/src/trpc/routers/auth.ts @@ -1,8 +1,8 @@ import { prisma } from "@rallly/database"; +import { posthog } from "@rallly/posthog/server"; import { generateOtp } from "@rallly/utils/nanoid"; import { z } from "zod"; -import { posthog } from "@/app/posthog"; import { isEmailBlocked } from "@/auth"; import { createToken, decryptToken } from "@/utils/session"; diff --git a/apps/web/src/trpc/routers/polls.ts b/apps/web/src/trpc/routers/polls.ts index 8366fb361..73df076ca 100644 --- a/apps/web/src/trpc/routers/polls.ts +++ b/apps/web/src/trpc/routers/polls.ts @@ -1,5 +1,6 @@ import type { PollStatus } from "@rallly/database"; import { prisma } from "@rallly/database"; +import { posthog } from "@rallly/posthog/server"; import { absoluteUrl, shortUrl } from "@rallly/utils/absolute-url"; import { nanoid } from "@rallly/utils/nanoid"; import { TRPCError } from "@trpc/server"; @@ -7,7 +8,6 @@ import dayjs from "dayjs"; import * as ics from "ics"; import { z } from "zod"; -import { posthog } from "@/app/posthog"; import { getEmailClient } from "@/utils/emails"; import { getTimeZoneAbbreviation } from "../../utils/date"; diff --git a/apps/web/src/utils/posthog.ts b/apps/web/src/utils/posthog.ts deleted file mode 100644 index c818cc570..000000000 --- a/apps/web/src/utils/posthog.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { usePostHog as usePostHogHook } from "posthog-js/react"; - -export const usePostHog = () => { - const posthog = usePostHogHook(); - return process.env.NEXT_PUBLIC_POSTHOG_API_KEY ? posthog : null; -}; diff --git a/packages/posthog/.eslintrc.js b/packages/posthog/.eslintrc.js new file mode 100644 index 000000000..a3b5a656d --- /dev/null +++ b/packages/posthog/.eslintrc.js @@ -0,0 +1,2 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = require("@rallly/eslint-config/preset")(__dirname); diff --git a/packages/posthog/package.json b/packages/posthog/package.json new file mode 100644 index 000000000..520ce1159 --- /dev/null +++ b/packages/posthog/package.json @@ -0,0 +1,18 @@ +{ + "name": "@rallly/posthog", + "version": "0.0.0", + "private": true, + "exports": { + "./server": "./src/server/index.ts", + "./client": "./src/client/index.ts", + "./next/middleware": "./src/next/middleware.ts" + }, + "dependencies": { + "posthog-js": "^1.178.0", + "posthog-node": "^4.2.1" + }, + "peerDependencies": { + "next": "^14.2.13", + "react": "^18.2.0" + } +} diff --git a/packages/posthog/src/client/index.ts b/packages/posthog/src/client/index.ts new file mode 100644 index 000000000..c3e322403 --- /dev/null +++ b/packages/posthog/src/client/index.ts @@ -0,0 +1,2 @@ +export { PostHogProvider } from "./provider"; +export { usePostHog } from "posthog-js/react"; diff --git a/packages/posthog/src/client/provider.tsx b/packages/posthog/src/client/provider.tsx new file mode 100644 index 000000000..b9f12ab3a --- /dev/null +++ b/packages/posthog/src/client/provider.tsx @@ -0,0 +1,36 @@ +"use client"; +import Cookies from "js-cookie"; +import posthog from "posthog-js"; +import { PostHogProvider as Provider } from "posthog-js/react"; +import React from "react"; + +import { POSTHOG_BOOTSTAP_DATA_COOKIE_NAME } from "../constants"; + +if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + let bootstrapData = {}; + try { + const cookieData = Cookies.get(POSTHOG_BOOTSTAP_DATA_COOKIE_NAME); + if (cookieData) { + bootstrapData = JSON.parse(cookieData); + } + } catch (error) { + console.warn("Failed to parse PostHog bootstrap data:", error); + } + + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, { + debug: false, + api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST, + capture_pageview: false, + capture_pageleave: true, + disable_session_recording: true, + enable_heatmaps: false, + persistence: "memory", + bootstrap: bootstrapData, + autocapture: false, + opt_out_capturing_by_default: false, + }); +} + +export function PostHogProvider(props: { children?: React.ReactNode }) { + return {props.children}; +} diff --git a/packages/posthog/src/constants.ts b/packages/posthog/src/constants.ts new file mode 100644 index 000000000..eb1ebedc9 --- /dev/null +++ b/packages/posthog/src/constants.ts @@ -0,0 +1 @@ +export const POSTHOG_BOOTSTAP_DATA_COOKIE_NAME = "posthog_bootstrap_data"; diff --git a/packages/posthog/src/next/middleware.ts b/packages/posthog/src/next/middleware.ts new file mode 100644 index 000000000..14a3a7d9a --- /dev/null +++ b/packages/posthog/src/next/middleware.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server"; +import { POSTHOG_BOOTSTAP_DATA_COOKIE_NAME } from "../constants"; + +const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY; + +export async function withPostHog( + res: NextResponse, + bootstrapData: { distinctID?: string }, +) { + if (!posthogApiKey) { + return; + } + + res.cookies.set({ + name: POSTHOG_BOOTSTAP_DATA_COOKIE_NAME, + value: JSON.stringify(bootstrapData), + httpOnly: false, + secure: true, + sameSite: "lax", + path: "/", + }); +} diff --git a/apps/web/src/app/posthog.ts b/packages/posthog/src/server/index.ts similarity index 67% rename from apps/web/src/app/posthog.ts rename to packages/posthog/src/server/index.ts index 828cf1055..7710c7200 100644 --- a/apps/web/src/app/posthog.ts +++ b/packages/posthog/src/server/index.ts @@ -1,13 +1,11 @@ import { waitUntil } from "@vercel/functions"; import { PostHog } from "posthog-node"; -import { env } from "@/env"; - function PostHogClient() { - if (!env.NEXT_PUBLIC_POSTHOG_API_KEY) return null; + if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) return null; - const posthogClient = new PostHog(env.NEXT_PUBLIC_POSTHOG_API_KEY, { - host: env.NEXT_PUBLIC_POSTHOG_API_HOST, + const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, { + host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST, flushAt: 1, flushInterval: 0, }); diff --git a/packages/posthog/tsconfig.json b/packages/posthog/tsconfig.json new file mode 100644 index 000000000..18a4f1351 --- /dev/null +++ b/packages/posthog/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@rallly/tsconfig/next.json", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"], +} diff --git a/yarn.lock b/yarn.lock index bf809b142..bdda04bf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12718,17 +12718,17 @@ postgres-range@^1.1.1: resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== -posthog-js@^1.154.0: - version "1.181.0" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.181.0.tgz#b2119f6a27b27297dee9540bfcd33eddab06905c" - integrity sha512-bI+J+f4E8x4JwbGtG6LReQv1Xvss01F6cs7UDlvffHySpVhNq4ptkNjV88B92IVEsrCtNYhy/TjFnGxk6RN0Qw== +posthog-js@^1.178.0: + version "1.178.0" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.178.0.tgz#80005798e6c67d4d6565a5648939a0f017b0879b" + integrity sha512-ILD4flNh72d5dycc4ZouKORlaVr+pDzl5TlZr1JgP0NBAoduHjhE7XZYwk7zdYkry7u0qAIpfv2306zJCW2vGg== dependencies: core-js "^3.38.1" fflate "^0.4.8" preact "^10.19.3" web-vitals "^4.2.0" -posthog-node@^4.0.1: +posthog-node@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-4.2.1.tgz#c9f077116bebd06dc65a3f9ae282d10db242c660" integrity sha512-l+fsjYEkTik3m/G0pE7gMr4qBJP84LhK779oQm6MBzhBGpd4By4qieTW+4FUAlNCyzQTynn3Nhsa50c0IELSxQ==