📈 Improvements to posthog analytics data (#1189)

This commit is contained in:
Luke Vella 2024-07-03 12:09:58 +01:00 committed by GitHub
parent 278713d57f
commit 587e11de17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 93 additions and 80 deletions

View file

@ -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")}
/> />

View file

@ -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,

View file

@ -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();
} }

View file

@ -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 }) => {

View file

@ -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;
}; };

View file

@ -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} />;
};

View file

@ -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:

View file

@ -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;

View file

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

View file

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