Skip to content

Commit

Permalink
feat: enable feature flag on announcement banner (#225)
Browse files Browse the repository at this point in the history
* fix: use typed env and styling

* chore: install posthog-node

* feat: identify user after logging in

* feat: feature flag controls for announcement banner

* fix: storybook failed build (see mapbox/node-pre-gyp/issues/661)

* fix: https://nextjs.org/docs/messages/sharp-missing-in-production

* fix: move feature flag evaluation to the implementation level instead of on the component level

* Revert "fix: storybook failed build (see mapbox/node-pre-gyp/issues/661)"

This reverts commit eaf61e6.

* chore: move posthog-node code to server dir
  • Loading branch information
davidlhw authored Aug 27, 2024
1 parent f33445f commit f010d94
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 15 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
"next-auth": "^4.24.7",
"next-themes": "^0.3.0",
"posthog-js": "^1.136.2",
"posthog-node": "^4.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.1",
"sass": "^1.72.0",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"superjson": "^2.2.1",
"tailwind-variants": "^0.2.1",
"validator": "^13.11.0",
Expand Down
7 changes: 6 additions & 1 deletion src/app/(school)/(reviews)/@header/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { AnnouncementBanner } from "@/modules/home/AnnouncementBanner";
import { getFeatureFlag } from "@/server/posthog";

export default function HomeHeader() {
export default async function HomeHeader() {
const isEnabled = await getFeatureFlag("announcements_carousel");
if (!isEnabled) {
return null;
}
return <AnnouncementBanner />;
}
21 changes: 11 additions & 10 deletions src/common/providers/analytics/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use client"
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
"use client";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { env } from "@/env.mjs";

if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
})
if (typeof window !== "undefined") {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: env.NEXT_PUBLIC_POSTHOG_HOST,
});
}

export function CSPostHogProvider({ children } : { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
8 changes: 5 additions & 3 deletions src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { emailValidationSchema } from "@/common/tools/zod/schemas";
import { db } from "@/server/db";
import randomId from "@/common/functions/randomId";
import { type Users } from "@prisma/client";
import { identifyUser } from "@/server/posthog";

type SessionUser = Omit<Users, "deprecatedPasswordDigest">;

Expand Down Expand Up @@ -74,7 +75,7 @@ export const authOptions: NextAuthOptions = {
)
) {
console.log(`User ${user.id} logged in with v1 credentials.`);
return user;
return await identifyUser(user);
}

const { data, error } = await signInWithEmail(
Expand Down Expand Up @@ -108,7 +109,7 @@ export const authOptions: NextAuthOptions = {
return null;
}

return await db.users.create({
const newUser = await db.users.create({
data: {
id: data.user.id,
email: data.user.email ?? c.data.email,
Expand All @@ -117,9 +118,10 @@ export const authOptions: NextAuthOptions = {
universityId: uniOfThisEmail.id,
},
});
return await identifyUser(newUser);
}
// Any object returned will be saved in `user` property of the JWT
return user;
return await identifyUser(user);
} else {
// If you return null then an error will be displayed advising the user to check their details.
return null;
Expand Down
12 changes: 12 additions & 0 deletions src/server/posthog/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PostHog } from "posthog-node";

import { env } from "@/env.mjs";

export function PostHogClient() {
const posthogClient = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
host: env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
});
return posthogClient;
}
38 changes: 38 additions & 0 deletions src/server/posthog/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { cookies } from "next/headers";
import { env } from "@/env.mjs";
import { PostHogClient } from "./client";
import { getServerAuthSession } from "@/server/auth";

/**
* Wrapper around PostHogClient.isFeatureEnabled for node.js (server-side)
* see https://posthog.com/docs/feature-flags/common-questions
* @param name user to be identified, should be returned on authentication
* @returns boolean indicating if the feature is enabled
*/
export async function getFeatureFlag(name: string) {
const session = await getServerAuthSession();
const cookieStore = cookies();

// posthog stores the distinct_id in a cookie with the key `ph_${POSTHOG_KEY}_posthog`
// see https://posthog.com/docs/libraries/js#persistence
const phCookieKey = `ph_${env.NEXT_PUBLIC_POSTHOG_KEY}_posthog`;
const phCookie = cookieStore.get(phCookieKey);

let distinct_id: string;
if (session?.user?.id) {
distinct_id = session.user.id;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} else if (phCookie && JSON.parse(phCookie.value).distinct_id) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
distinct_id = JSON.parse(phCookie.value).distinct_id as string;
} else {
// unlikely to happen, only if the user is not logged in and the cookie is not set
distinct_id = crypto.randomUUID();
}

const posthog = PostHogClient();
const flag = !!(await posthog.isFeatureEnabled(name, distinct_id));
await posthog.shutdown();

return flag;
}
19 changes: 19 additions & 0 deletions src/server/posthog/identify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PostHogClient } from "./client";
import { type Users } from "@prisma/client";

/**
* Linking events to specific users
* see https://posthog.com/docs/product-analytics/identify
* @param user user to be identified, should be returned on authentication
* @returns same `user` object
*/
export async function identifyUser(user: Users) {
const posthog = PostHogClient();

// ignoring unnecessary fields
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, deprecatedPasswordDigest, ...personProperties } = user;
posthog.identify({ distinctId: id, properties: personProperties });
await posthog.shutdown();
return user;
}
3 changes: 3 additions & 0 deletions src/server/posthog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./identify";
export * from "./flags";
export * from "./client";
Loading

0 comments on commit f010d94

Please sign in to comment.