Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 17 additions & 31 deletions src/app/api/evaluate-flags/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { PostHog } from "posthog-node";
import { getLogEmitter } from "@/lib/log-emitter";
import { logEmitter } from "@/lib/log-emitter";

const posthogClient = new PostHog(
process.env.NEXT_PUBLIC_POSTHOG_KEY!,
process.env.POSTHOG_FEATURE_FLAG_API_KEY!,
{
host: process.env.POSTHOG_HOST || "https://us.i.posthog.com",
flushAt: 1,
Expand All @@ -21,18 +21,9 @@ export async function POST(request: NextRequest) {
onlyEvaluateLocally
} = body;

const logEmitter = getLogEmitter();

logEmitter.emit("log", {
task: "flag-evaluation",
message: `Evaluating flags for ${distinctId}`,
data: {
method: evaluationMethod,
localOnly: onlyEvaluateLocally,
properties: personProperties
}
});
logEmitter.info(`Evaluating flags for ${distinctId} (method: ${evaluationMethod}, localOnly: ${onlyEvaluateLocally})`);

const startTime = performance.now();
let result = {};

if (evaluationMethod === "server-side" || evaluationMethod === "server-side-local") {
Expand All @@ -45,42 +36,37 @@ export async function POST(request: NextRequest) {
}
);

const elapsedTime = Math.round(performance.now() - startTime);

result = {
flags,
evaluationMethod,
distinctId,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
elapsedTimeMs: elapsedTime
};

logEmitter.emit("log", {
task: "flag-evaluation",
message: `Flags evaluated successfully`,
data: result
});
logEmitter.success(`Flags evaluated successfully for ${distinctId} in ${elapsedTime}ms`);
} catch (error) {
logEmitter.emit("log", {
task: "flag-evaluation",
message: `Error evaluating flags: ${error}`,
data: { error: String(error) }
});
const elapsedTime = Math.round(performance.now() - startTime);
logEmitter.error(`Error evaluating flags after ${elapsedTime}ms: ${String(error)}`);
throw error;
}
} else {
const elapsedTime = Math.round(performance.now() - startTime);

result = {
message: "Client-side evaluation selected - flags evaluated on client",
evaluationMethod,
distinctId,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
elapsedTimeMs: elapsedTime
};

logEmitter.emit("log", {
task: "flag-evaluation",
message: `Client-side evaluation - no server evaluation performed`,
data: result
});
logEmitter.info(`Client-side evaluation - no server evaluation performed for ${distinctId} (${elapsedTime}ms)`);
}

await posthogClient.shutdownAsync();
await posthogClient.shutdown();

return NextResponse.json(result);
} catch (error) {
Expand Down
55 changes: 55 additions & 0 deletions src/app/api/feature-flags/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextResponse } from "next/server";

export async function GET() {
try {
const apiKey = process.env.POSTHOG_PERSONAL_API_KEY;
const projectId = process.env.POSTHOG_PROJECT_ID;
const host = process.env.POSTHOG_HOST || "https://us.i.posthog.com";

if (!apiKey) {
return NextResponse.json(
{ error: "Missing PostHog personal API key" },
{ status: 500 }
);
}

if (!projectId) {
return NextResponse.json(
{ error: "Missing PostHog project ID. Add POSTHOG_PROJECT_ID to your .env file" },
{ status: 500 }
);
}

// Fetch feature flags for the specific project
const response = await fetch(
`${host}/api/projects/${projectId}/feature_flags/`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
},
}
);

if (!response.ok) {
const errorText = await response.text();
throw new Error(`PostHog API error: ${response.status} - ${errorText}`);
}

const data = await response.json();

// Extract just the flag keys and names
const flags = data.results.map((flag: any) => ({
key: flag.key,
name: flag.name,
active: flag.active,
}));

return NextResponse.json({ flags });
} catch (error) {
console.error("Error fetching feature flags:", error);
return NextResponse.json(
{ error: "Failed to fetch feature flags", details: String(error) },
{ status: 500 }
);
}
}
41 changes: 15 additions & 26 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
"use client";

import { useState } from "react";
import { PaneOne } from "@/components/pane-one";
import { PaneTwo } from "@/components/pane-two";
import { PaneThree } from "@/components/pane-three";
import { FeatureFlagDemo } from "@/components/feature-flag-demo";

export default function Home() {
const [selectedFlag, setSelectedFlag] = useState<string>("new-ui-flow");

return (
<div className="h-screen w-full p-16">
<ResizablePanelGroup
direction="horizontal"
className="rounded-lg border"
>
<ResizablePanel defaultSize={50}>
<PaneOne />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={50}>
<PaneTwo />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<PaneThree />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 h-full items-start">
<div className="h-full">
<PaneOne selectedFlag={selectedFlag} onFlagChange={setSelectedFlag} />
</div>
<div className="h-full flex flex-col gap-6">
<FeatureFlagDemo selectedFlag={selectedFlag} />
<PaneThree />
</div>
</div>
</div>
);
}
70 changes: 70 additions & 0 deletions src/components/feature-flag-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import posthog from "posthog-js";

interface FeatureFlagDemoProps {
selectedFlag: string;
}

export function FeatureFlagDemo({ selectedFlag }: FeatureFlagDemoProps) {
const [isNewUI, setIsNewUI] = useState(false);

useEffect(() => {
// Listen for feature flag changes (including initial load)
const handleFlagChange = () => {
const flagValue = posthog.isFeatureEnabled(selectedFlag);
setIsNewUI(flagValue === true);
};

// onFeatureFlags callback is triggered when flags are loaded, including initial load
posthog.onFeatureFlags(handleFlagChange);

// Cleanup listener on unmount
return () => {
// PostHog doesn't have a direct way to remove listeners, but this ensures cleanup
};
}, [selectedFlag]);

return (
<Card className="w-full">
<CardHeader>
<CardTitle>Feature Flag Demo</CardTitle>
<CardDescription>
UI changes based on the {selectedFlag} feature flag
</CardDescription>
</CardHeader>
<CardContent>
{isNewUI ? (
<div className="p-6 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-white">
<h3 className="text-2xl font-bold mb-2">🎉 New UI</h3>
<p className="text-lg">
You&apos;re seeing the new, modern interface with enhanced
features!
</p>
<div className="mt-4 flex gap-2">
<div className="h-2 w-2 rounded-full bg-white animate-pulse" />
<div className="h-2 w-2 rounded-full bg-white animate-pulse delay-75" />
<div className="h-2 w-2 rounded-full bg-white animate-pulse delay-150" />
</div>
</div>
) : (
<div className="p-6 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 border-gray-300 dark:border-gray-700">
<h3 className="text-xl font-semibold mb-2">Old UI</h3>
<p className="text-muted-foreground">
This is the classic interface. Enable the {selectedFlag} flag to
see the updated design.
</p>
</div>
)}
</CardContent>
</Card>
);
}
Loading