Compare commits

...
Author SHA1 Message Date
Cursor Agentandmarcftone 40860c3eff Refactor: Abstract PostHog tracking into analytics helper
- Move server-side PostHog client into lib/analytics/index.ts
- Create 'analytics' object with capture() and identify() methods
- Mirrors the client-side useAnalytics() hook pattern
- Remove separate posthog-server.ts file
- Update invitation acceptance to use analytics.capture()

Co-authored-by: marcftone <marcftone@gmail.com>
2026-02-04 02:04:31 +00:00
Cursor Agentandmarcftone 92b89f26af Add PostHog analytics tracking for team invitation acceptance
- Install posthog-node for server-side tracking
- Create posthog-server.ts utility with capturePostHogEvent function
- Add PostHog tracking when user accepts team invitation
- Event 'Team Member Invitation Accepted' includes teamId and userId properties

Co-authored-by: marcftone <marcftone@gmail.com>
2026-02-03 23:25:18 +00:00
4 changed files with 136 additions and 5 deletions
+102 -4
View File
@@ -1,9 +1,14 @@
import { emptyAnalytics, jitsuAnalytics } from "@jitsu/js";
import { PostHog } from "posthog-node";
import { posthog } from "posthog-js";
import { getPostHogConfig } from "@/lib/posthog";
import { AnalyticsEvents } from "@/lib/types";
// =============================================================================
// Client-side Analytics (PostHog via posthog-js)
// =============================================================================
export function useAnalytics() {
const isPostHogEnabled = getPostHogConfig();
@@ -38,8 +43,11 @@ export function useAnalytics() {
};
}
// For server-side tracking
const analytics =
// =============================================================================
// Server-side Analytics (Jitsu)
// =============================================================================
const jitsu =
process.env.JITSU_HOST && process.env.JITSU_WRITE_KEY
? jitsuAnalytics({
host: process.env.JITSU_HOST,
@@ -47,5 +55,95 @@ const analytics =
})
: emptyAnalytics;
export const identifyUser = (userId: string) => analytics.identify(userId);
export const trackAnalytics = (args: AnalyticsEvents) => analytics.track(args);
export const identifyUser = (userId: string) => jitsu.identify(userId);
export const trackAnalytics = (args: AnalyticsEvents) => jitsu.track(args);
// =============================================================================
// Server-side Analytics (PostHog via posthog-node)
// =============================================================================
let posthogServerClient: PostHog | null = null;
function getPostHogServerClient(): PostHog | null {
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
if (!postHogKey) {
return null;
}
if (!posthogServerClient) {
posthogServerClient = new PostHog(postHogKey, {
host: "https://eu.i.posthog.com",
// Flush events immediately in serverless environments
flushAt: 1,
flushInterval: 0,
});
}
return posthogServerClient;
}
/**
* Server-side analytics helper for PostHog.
* Similar pattern to the client-side useAnalytics hook.
*/
export const analytics = {
/**
* Capture an analytic event server-side.
*
* @param distinctId The user's unique identifier (typically email or userId).
* @param event The event name.
* @param properties Properties to attach to the event.
*/
capture: async ({
distinctId,
event,
properties,
}: {
distinctId: string;
event: string;
properties?: Record<string, unknown>;
}): Promise<void> => {
const client = getPostHogServerClient();
if (!client) {
return;
}
client.capture({
distinctId,
event,
properties,
});
// Flush immediately to ensure event is sent before response
await client.flush();
},
/**
* Identify a user server-side.
*
* @param distinctId The user's unique identifier (typically email or userId).
* @param properties User properties to set.
*/
identify: async ({
distinctId,
properties,
}: {
distinctId: string;
properties?: Record<string, unknown>;
}): Promise<void> => {
const client = getPostHogServerClient();
if (!client) {
return;
}
client.identify({
distinctId,
properties,
});
await client.flush();
},
};
+22
View File
@@ -113,6 +113,7 @@
"pdf-lib": "^1.17.1",
"postcss": "^8.5.6",
"posthog-js": "^1.336.1",
"posthog-node": "^5.21.2",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.10.1",
@@ -17590,6 +17591,27 @@
"node": ">=14"
}
},
"node_modules/posthog-node": {
"version": "5.21.2",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.21.2.tgz",
"integrity": "sha512-Jehlu0KguL1LLyUczCt86OtA5INmeStK3zcgbv1BSyMcNxs0HP3GQogBrYhwhqHsk6JopiFFVpJyZEoXOUMhGw==",
"license": "MIT",
"dependencies": {
"@posthog/core": "1.10.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/posthog-node/node_modules/@posthog/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.10.0.tgz",
"integrity": "sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/preact": {
"version": "10.28.2",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
+1
View File
@@ -124,6 +124,7 @@
"pdf-lib": "^1.17.1",
"postcss": "^8.5.6",
"posthog-js": "^1.336.1",
"posthog-node": "^5.21.2",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.10.1",
+11 -1
View File
@@ -3,7 +3,7 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth";
import { identifyUser, trackAnalytics } from "@/lib/analytics";
import { analytics, identifyUser, trackAnalytics } from "@/lib/analytics";
import { errorhandler } from "@/lib/errorHandler";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
@@ -120,6 +120,16 @@ export default async function handle(
teamId: teamId,
});
// Track in PostHog
await analytics.capture({
distinctId: invitation.email,
event: "Team Member Invitation Accepted",
properties: {
teamId: teamId,
userId: userId,
},
});
// delete the invitation record after user is successfully added to the team
await prisma.invitation.delete({
where: {