mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-03 20:26:03 +02:00
📈 Improvements to posthog analytics data (#1189)
This commit is contained in:
parent
278713d57f
commit
587e11de17
10 changed files with 93 additions and 80 deletions
|
@ -7,7 +7,6 @@ import { AlertTriangleIcon, UserIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { getProviders, signIn, useSession } from "next-auth/react";
|
import { getProviders, signIn, useSession } from "next-auth/react";
|
||||||
import { usePostHog } from "posthog-js/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -38,7 +37,6 @@ export function LoginForm() {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const queryClient = trpc.useUtils();
|
const queryClient = trpc.useUtils();
|
||||||
const [email, setEmail] = React.useState<string>();
|
const [email, setEmail] = React.useState<string>();
|
||||||
const posthog = usePostHog();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const callbackUrl = searchParams?.get("callbackUrl") ?? "/";
|
const callbackUrl = searchParams?.get("callbackUrl") ?? "/";
|
||||||
|
|
||||||
|
@ -128,19 +126,15 @@ export function LoginForm() {
|
||||||
email,
|
email,
|
||||||
token: code,
|
token: code,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error("Failed to authenticate user");
|
throw new Error("Failed to authenticate user");
|
||||||
} else {
|
|
||||||
await queryClient.invalidate();
|
|
||||||
const s = await session.update();
|
|
||||||
if (s?.user) {
|
|
||||||
posthog?.identify(s.user.id, {
|
|
||||||
email: s.user.email,
|
|
||||||
name: s.user.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
router.push(callbackUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await queryClient.invalidate();
|
||||||
|
await session.update();
|
||||||
|
|
||||||
|
router.push(callbackUrl);
|
||||||
}}
|
}}
|
||||||
email={getValues("email")}
|
email={getValues("email")}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -70,14 +70,7 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
queryClient.invalidate();
|
queryClient.invalidate();
|
||||||
|
|
||||||
posthog?.identify(res.user.id, {
|
posthog?.identify(res.user.id);
|
||||||
email: res.user.email,
|
|
||||||
name: res.user.name,
|
|
||||||
timeZone,
|
|
||||||
locale,
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog?.capture("register");
|
|
||||||
|
|
||||||
signIn("registration-token", {
|
signIn("registration-token", {
|
||||||
token,
|
token,
|
||||||
|
|
|
@ -28,10 +28,7 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
|
||||||
const updatedSession = await session.update();
|
const updatedSession = await session.update();
|
||||||
if (updatedSession) {
|
if (updatedSession) {
|
||||||
// identify the user in posthog
|
// identify the user in posthog
|
||||||
posthog?.identify(updatedSession.user.id, {
|
posthog?.identify(updatedSession.user.id);
|
||||||
email: updatedSession.user.email,
|
|
||||||
name: updatedSession.user.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
await trpcUtils.invalidate();
|
await trpcUtils.invalidate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import React from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useTranslation } from "@/app/i18n/client";
|
import { useTranslation } from "@/app/i18n/client";
|
||||||
|
import { Spinner } from "@/components/spinner";
|
||||||
|
import { useSubscription } from "@/contexts/plan";
|
||||||
import { PostHogProvider } from "@/contexts/posthog";
|
import { PostHogProvider } from "@/contexts/posthog";
|
||||||
import { PreferencesProvider } from "@/contexts/preferences";
|
import { PreferencesProvider } from "@/contexts/preferences";
|
||||||
|
|
||||||
|
@ -15,6 +17,7 @@ const userSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.string().email().nullable(),
|
email: z.string().email().nullable(),
|
||||||
isGuest: z.boolean(),
|
isGuest: z.boolean(),
|
||||||
|
tier: z.enum(["guest", "hobby", "pro"]),
|
||||||
timeZone: z.string().nullish(),
|
timeZone: z.string().nullish(),
|
||||||
timeFormat: z.enum(["hours12", "hours24"]).nullish(),
|
timeFormat: z.enum(["hours12", "hours24"]).nullish(),
|
||||||
weekStart: z.number().min(0).max(6).nullish(),
|
weekStart: z.number().min(0).max(6).nullish(),
|
||||||
|
@ -50,15 +53,22 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
|
||||||
|
|
||||||
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
const user = session.data?.user;
|
const user = session.data?.user;
|
||||||
|
const subscription = useSubscription();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return (
|
||||||
|
<div className="flex h-screen items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isGuest = !user.email;
|
||||||
|
const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider
|
<UserContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -66,7 +76,8 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
id: user.id as string,
|
id: user.id as string,
|
||||||
name: user.name ?? t("guest"),
|
name: user.name ?? t("guest"),
|
||||||
email: user.email || null,
|
email: user.email || null,
|
||||||
isGuest: user.email === null,
|
isGuest: !user.email,
|
||||||
|
tier,
|
||||||
},
|
},
|
||||||
refresh: session.update,
|
refresh: session.update,
|
||||||
ownsObject: ({ userId }) => {
|
ownsObject: ({ userId }) => {
|
||||||
|
|
|
@ -3,15 +3,12 @@ import { Badge } from "@rallly/ui/badge";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
|
||||||
import { isSelfHosted } from "@/utils/constants";
|
import { isSelfHosted } from "@/utils/constants";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
export const useSubscription = () => {
|
export const useSubscription = () => {
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
const { data } = trpc.user.subscription.useQuery(undefined, {
|
const { data } = trpc.user.subscription.useQuery(undefined, {
|
||||||
enabled: !isSelfHosted && user.isGuest === false,
|
enabled: !isSelfHosted,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSelfHosted) {
|
if (isSelfHosted) {
|
||||||
|
@ -20,12 +17,6 @@ export const useSubscription = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isGuest) {
|
|
||||||
return {
|
|
||||||
active: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { PostHogProvider as Provider } from "posthog-js/react";
|
import { PostHogProvider as Provider } from "posthog-js/react";
|
||||||
import { useMount } from "react-use";
|
import { useMount } from "react-use";
|
||||||
|
@ -6,15 +7,12 @@ import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
type PostHogProviderProps = React.PropsWithChildren;
|
type PostHogProviderProps = React.PropsWithChildren;
|
||||||
|
|
||||||
const PostHogProviderInner = (props: PostHogProviderProps) => {
|
export function PostHogProvider(props: PostHogProviderProps) {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
// initalize posthog with our user id
|
// initalize posthog with our user id
|
||||||
if (
|
if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
|
||||||
typeof window !== "undefined" &&
|
|
||||||
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
|
|
||||||
) {
|
|
||||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
|
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
|
||||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
|
api_host: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
|
||||||
opt_out_capturing_by_default: false,
|
opt_out_capturing_by_default: false,
|
||||||
|
@ -31,12 +29,4 @@ const PostHogProviderInner = (props: PostHogProviderProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Provider client={posthog}>{props.children}</Provider>;
|
return <Provider client={posthog}>{props.children}</Provider>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const PostHogProvider = (props: PostHogProviderProps) => {
|
|
||||||
if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
|
|
||||||
return <>{props.children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PostHogProviderInner {...props} />;
|
|
||||||
};
|
|
||||||
|
|
|
@ -30,7 +30,11 @@ const validatedWebhook = async (req: NextApiRequest) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const metadataSchema = z.object({
|
const checkoutMetadataSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscriptionMetadataSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,7 +63,7 @@ async function stripeApiHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId } = metadataSchema.parse(checkoutSession.metadata);
|
const { userId } = checkoutMetadataSchema.parse(checkoutSession.metadata);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
res.status(400).send("Missing client reference ID");
|
res.status(400).send("Missing client reference ID");
|
||||||
|
@ -86,10 +90,13 @@ async function stripeApiHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
event: "upgrade",
|
event: "upgrade",
|
||||||
properties: {
|
properties: {
|
||||||
interval: subscription.items.data[0].plan.interval,
|
interval: subscription.items.data[0].plan.interval,
|
||||||
|
$set: {
|
||||||
|
tier: "pro",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Sentry.captureMessage("Failed to track upgrade event");
|
Sentry.captureException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -133,6 +140,22 @@ async function stripeApiHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = subscriptionMetadataSchema.parse(subscription.metadata);
|
||||||
|
posthog?.capture({
|
||||||
|
event: "subscription change",
|
||||||
|
distinctId: data.userId,
|
||||||
|
properties: {
|
||||||
|
type: event.type,
|
||||||
|
$set: {
|
||||||
|
tier: isActive ? "pro" : "hobby",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Sentry.captureException(e);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -174,9 +174,32 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
|
||||||
signOut: "/logout",
|
signOut: "/logout",
|
||||||
error: "/auth/error",
|
error: "/auth/error",
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
signIn({ user, account }) {
|
||||||
|
posthog?.capture({
|
||||||
|
distinctId: user.id,
|
||||||
|
event: "login",
|
||||||
|
properties: {
|
||||||
|
method: account?.provider,
|
||||||
|
$set: {
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
locale: user.locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
signOut({ session }) {
|
||||||
|
posthog?.capture({
|
||||||
|
distinctId: session.user.id,
|
||||||
|
event: "logout",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async signIn({ user, email, account, profile }) {
|
async signIn({ user, email, profile }) {
|
||||||
const distinctId = user.email ?? user.id;
|
const distinctId = user.id;
|
||||||
// prevent sign in if email is not verified
|
// prevent sign in if email is not verified
|
||||||
if (
|
if (
|
||||||
profile &&
|
profile &&
|
||||||
|
@ -221,22 +244,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
|
||||||
if (session && session.user.email === null) {
|
if (session && session.user.email === null) {
|
||||||
await mergeGuestsIntoUser(user.id, [session.user.id]);
|
await mergeGuestsIntoUser(user.id, [session.user.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
posthog?.identify({
|
|
||||||
distinctId,
|
|
||||||
properties: {
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog?.capture({
|
|
||||||
distinctId,
|
|
||||||
event: "login",
|
|
||||||
properties: {
|
|
||||||
method: account?.provider,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const auth = router({
|
||||||
locale: z.string().optional(),
|
locale: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const payload = await decryptToken<RegistrationTokenPayload>(input.token);
|
const payload = await decryptToken<RegistrationTokenPayload>(input.token);
|
||||||
|
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
|
@ -99,6 +99,19 @@ export const auth = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.posthogClient?.capture({
|
||||||
|
event: "register",
|
||||||
|
distinctId: user.id,
|
||||||
|
properties: {
|
||||||
|
$set: {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
timeZone: input.timeZone,
|
||||||
|
locale: input.locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return { ok: true, user };
|
return { ok: true, user };
|
||||||
}),
|
}),
|
||||||
getUserPermission: publicProcedure
|
getUserPermission: publicProcedure
|
||||||
|
|
|
@ -923,13 +923,7 @@ export const polls = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
waitUntil(
|
waitUntil(Promise.all([emailToHost, ...emailsToParticipants]));
|
||||||
Promise.all([
|
|
||||||
emailToHost,
|
|
||||||
...emailsToParticipants,
|
|
||||||
ctx.posthogClient?.flushAsync(),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
reopen: possiblyPublicProcedure
|
reopen: possiblyPublicProcedure
|
||||||
|
|
Loading…
Add table
Reference in a new issue