♻️ Add abstractions for tracking server-side events (#1143)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Luke Vella 2024-06-10 20:23:49 +01:00 committed by GitHub
parent 81d2f2c0bd
commit d43bc631f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 69 additions and 12 deletions

View file

@ -36,6 +36,7 @@
"@trpc/client": "^10.13.0", "@trpc/client": "^10.13.0",
"@trpc/next": "^10.13.0", "@trpc/next": "^10.13.0",
"@trpc/react-query": "^10.13.0", "@trpc/react-query": "^10.13.0",
"@vercel/functions": "^1.0.2",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",

View file

@ -1,6 +1,7 @@
import { waitUntil } from "@vercel/functions";
import { PostHog } from "posthog-node"; import { PostHog } from "posthog-node";
export function PostHogClient() { function PostHogClient() {
if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) return null; if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) return null;
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, { const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
@ -10,3 +11,13 @@ export function PostHogClient() {
}); });
return posthogClient; return posthogClient;
} }
export const posthog = PostHogClient();
export function posthogApiHandler() {
try {
waitUntil(Promise.all([posthog?.shutdownAsync()]));
} catch (error) {
console.error("Failed to flush PostHog events:", error);
}
}

View file

@ -1,11 +1,14 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { posthogApiHandler } from "@/app/posthog";
import { AuthApiRoute } from "@/utils/auth"; import { AuthApiRoute } from "@/utils/auth";
import { composeApiHandlers } from "@/utils/next";
export default async function auth(req: NextApiRequest, res: NextApiResponse) { export default async function auth(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "HEAD") { if (req.method === "HEAD") {
return res.status(200).end(); res.status(200).end();
res.setHeader('Content-Length', '0');
} else {
return composeApiHandlers(AuthApiRoute, posthogApiHandler)(req, res);
} }
return AuthApiRoute(req, res);
} }

View file

@ -3,10 +3,12 @@ import { AppRouter, appRouter } from "@rallly/backend/trpc/routers";
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { createNextApiHandler } from "@trpc/server/adapters/next"; import { createNextApiHandler } from "@trpc/server/adapters/next";
import { posthog, posthogApiHandler } from "@/app/posthog";
import { absoluteUrl, shortUrl } from "@/utils/absolute-url"; import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
import { getServerSession, isEmailBlocked } from "@/utils/auth"; import { getServerSession, isEmailBlocked } from "@/utils/auth";
import { isSelfHosted } from "@/utils/constants"; import { isSelfHosted } from "@/utils/constants";
import { emailClient } from "@/utils/emails"; import { emailClient } from "@/utils/emails";
import { composeApiHandlers } from "@/utils/next";
export const config = { export const config = {
api: { api: {
@ -14,10 +16,10 @@ export const config = {
}, },
}; };
export default createNextApiHandler<AppRouter>({ const trpcApiHandler = createNextApiHandler<AppRouter>({
router: appRouter, router: appRouter,
createContext: async (opts) => { createContext: async (opts) => {
return createTRPCContext(opts, { const res = createTRPCContext(opts, {
async getUser({ req, res }) { async getUser({ req, res }) {
const session = await getServerSession(req, res); const session = await getServerSession(req, res);
@ -30,12 +32,15 @@ export default createNextApiHandler<AppRouter>({
isGuest: session.user.email === null, isGuest: session.user.email === null,
}; };
}, },
posthogClient: posthog || undefined,
emailClient, emailClient,
isSelfHosted, isSelfHosted,
isEmailBlocked, isEmailBlocked,
absoluteUrl, absoluteUrl,
shortUrl, shortUrl,
}); });
return res;
}, },
onError({ error }) { onError({ error }) {
if (error.code === "INTERNAL_SERVER_ERROR") { if (error.code === "INTERNAL_SERVER_ERROR") {
@ -43,3 +48,5 @@ export default createNextApiHandler<AppRouter>({
} }
}, },
}); });
export default composeApiHandlers(trpcApiHandler, posthogApiHandler);

View file

@ -17,7 +17,7 @@ import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from "next-auth/providers/google";
import { Provider } from "next-auth/providers/index"; import { Provider } from "next-auth/providers/index";
import { PostHogClient } from "@/app/posthog"; import { posthog } from "@/app/posthog";
import { absoluteUrl } from "@/utils/absolute-url"; import { absoluteUrl } from "@/utils/absolute-url";
import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter"; import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter";
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user"; import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
@ -176,7 +176,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
}, },
callbacks: { callbacks: {
async signIn({ user, email, account, profile }) { async signIn({ user, email, account, profile }) {
const posthog = PostHogClient();
const distinctId = user.email ?? user.id; const distinctId = user.email ?? user.id;
// prevent sign in if email is not verified // prevent sign in if email is not verified
if ( if (
@ -191,7 +190,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
reason: "email not verified", reason: "email not verified",
}, },
}); });
await posthog?.shutdownAsync();
return false; return false;
} }
// Make sure email is allowed // Make sure email is allowed
@ -224,6 +222,14 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
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({ posthog?.capture({
distinctId, distinctId,
event: "login", event: "login",
@ -231,7 +237,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
method: account?.provider, method: account?.provider,
}, },
}); });
await posthog?.shutdownAsync();
} }
return true; return true;

View file

@ -0,0 +1,12 @@
import { NextApiHandler } from "next";
export function composeApiHandlers(...fns: NextApiHandler[]): NextApiHandler {
return async (req, res) => {
for (const fn of fns) {
await fn(req, res);
if (res.writableEnded) {
return;
}
}
};
}

View file

@ -12,7 +12,6 @@
"@rallly/database": "*", "@rallly/database": "*",
"@rallly/emails": "*", "@rallly/emails": "*",
"@rallly/utils": "*", "@rallly/utils": "*",
"@vercel/functions": "^1.0.2",
"@trpc/server": "^10.13.0", "@trpc/server": "^10.13.0",
"iron-session": "^6.3.1", "iron-session": "^6.3.1",
"spacetime": "^7.4.7", "spacetime": "^7.4.7",

View file

@ -1,6 +1,7 @@
import { EmailClient } from "@rallly/emails"; import { EmailClient } from "@rallly/emails";
import { inferAsyncReturnType, TRPCError } from "@trpc/server"; import { inferAsyncReturnType, TRPCError } from "@trpc/server";
import { CreateNextContextOptions } from "@trpc/server/adapters/next"; import { CreateNextContextOptions } from "@trpc/server/adapters/next";
import type { PostHog } from "posthog-node";
export type GetUserFn = (opts: CreateNextContextOptions) => Promise<{ export type GetUserFn = (opts: CreateNextContextOptions) => Promise<{
id: string; id: string;
@ -12,6 +13,7 @@ export interface TRPCContextParams {
emailClient: EmailClient; emailClient: EmailClient;
isSelfHosted: boolean; isSelfHosted: boolean;
isEmailBlocked?: (email: string) => boolean; isEmailBlocked?: (email: string) => boolean;
posthogClient?: PostHog;
/** /**
* Takes a relative path and returns an absolute URL to the app * Takes a relative path and returns an absolute URL to the app
* @param path * @param path

View file

@ -626,6 +626,7 @@ export const polls = router({
}, },
select: { select: {
id: true, id: true,
createdAt: true,
timeZone: true, timeZone: true,
title: true, title: true,
location: true, location: true,
@ -859,7 +860,23 @@ export const polls = router({
}); });
}); });
waitUntil(Promise.all([emailToHost, ...emailsToParticipants])); ctx.posthogClient?.capture({
distinctId: ctx.user.id,
event: "finalize poll",
properties: {
number_of_participants: poll.participants.length,
number_of_attendees: attendees.length,
dayjs_since_created: dayjs().diff(poll.createdAt, "day"),
},
});
waitUntil(
Promise.all([
emailToHost,
...emailsToParticipants,
ctx.posthogClient?.flushAsync(),
]),
);
} }
}), }),
reopen: possiblyPublicProcedure reopen: possiblyPublicProcedure