♻️ 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/next": "^10.13.0",
"@trpc/react-query": "^10.13.0",
"@vercel/functions": "^1.0.2",
"accept-language-parser": "^1.5.0",
"autoprefixer": "^10.4.13",
"class-variance-authority": "^0.7.0",

View file

@ -1,6 +1,7 @@
import { waitUntil } from "@vercel/functions";
import { PostHog } from "posthog-node";
export function PostHogClient() {
function PostHogClient() {
if (!process.env.NEXT_PUBLIC_POSTHOG_API_KEY) return null;
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_API_KEY, {
@ -10,3 +11,13 @@ export function 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 { posthogApiHandler } from "@/app/posthog";
import { AuthApiRoute } from "@/utils/auth";
import { composeApiHandlers } from "@/utils/next";
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
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 { createNextApiHandler } from "@trpc/server/adapters/next";
import { posthog, posthogApiHandler } from "@/app/posthog";
import { absoluteUrl, shortUrl } from "@/utils/absolute-url";
import { getServerSession, isEmailBlocked } from "@/utils/auth";
import { isSelfHosted } from "@/utils/constants";
import { emailClient } from "@/utils/emails";
import { composeApiHandlers } from "@/utils/next";
export const config = {
api: {
@ -14,10 +16,10 @@ export const config = {
},
};
export default createNextApiHandler<AppRouter>({
const trpcApiHandler = createNextApiHandler<AppRouter>({
router: appRouter,
createContext: async (opts) => {
return createTRPCContext(opts, {
const res = createTRPCContext(opts, {
async getUser({ req, res }) {
const session = await getServerSession(req, res);
@ -30,12 +32,15 @@ export default createNextApiHandler<AppRouter>({
isGuest: session.user.email === null,
};
},
posthogClient: posthog || undefined,
emailClient,
isSelfHosted,
isEmailBlocked,
absoluteUrl,
shortUrl,
});
return res;
},
onError({ 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 { Provider } from "next-auth/providers/index";
import { PostHogClient } from "@/app/posthog";
import { posthog } from "@/app/posthog";
import { absoluteUrl } from "@/utils/absolute-url";
import { CustomPrismaAdapter } from "@/utils/auth/custom-prisma-adapter";
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
@ -176,7 +176,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
},
callbacks: {
async signIn({ user, email, account, profile }) {
const posthog = PostHogClient();
const distinctId = user.email ?? user.id;
// prevent sign in if email is not verified
if (
@ -191,7 +190,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
reason: "email not verified",
},
});
await posthog?.shutdownAsync();
return false;
}
// Make sure email is allowed
@ -224,6 +222,14 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
await mergeGuestsIntoUser(user.id, [session.user.id]);
}
posthog?.identify({
distinctId,
properties: {
name: user.name,
email: user.email,
},
});
posthog?.capture({
distinctId,
event: "login",
@ -231,7 +237,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
method: account?.provider,
},
});
await posthog?.shutdownAsync();
}
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/emails": "*",
"@rallly/utils": "*",
"@vercel/functions": "^1.0.2",
"@trpc/server": "^10.13.0",
"iron-session": "^6.3.1",
"spacetime": "^7.4.7",

View file

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

View file

@ -626,6 +626,7 @@ export const polls = router({
},
select: {
id: true,
createdAt: true,
timeZone: true,
title: 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