mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-01 11:16:32 +02:00
♻️ 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:
parent
81d2f2c0bd
commit
d43bc631f1
9 changed files with 69 additions and 12 deletions
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
12
apps/web/src/utils/next.ts
Normal file
12
apps/web/src/utils/next.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue