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==