Client Side Logging
We currently have our flags data being served from Statsig, we'll now implement client side event and exposure logging using the embed pattern.
We'll embed Statsig evaluation context and evaluation data into the static response from the routing middleware
import { type NextRequest } from "next/server";
import { embedBootstrapData, precompute } from "flags/next";
import { productFlags } from "@/flags";
import { getStableId } from "./lib/get-stable-id";
import { identify } from "./flags";
import { statsigAdapter } from "@flags-sdk/statsig";
export const config = {
matcher: ["/", "/cart"],
};
export async function middleware(request: NextRequest) {
const stableId = await getStableId();
const code = await precompute(productFlags);
// rewrites the request to the variant for this flag combination
const nextUrl = new URL(
`/${code}${request.nextUrl.pathname}${request.nextUrl.search}`,
request.url
);
const modifiedRequest = new Request(nextUrl, { ...request });
const [statsig, statsigUser, response] = await Promise.all([
statsigAdapter.initialize(),
identify(),
fetch(modifiedRequest),
]);
const clientInitializeResponse = statsig.getClientInitializeResponse(
statsigUser,
{ hash: "djb2" }
);
const modifiedResponse = embedBootstrapData(response, {
clientInitializeResponse,
statsigUser,
});
const h = new Headers(modifiedResponse.headers);
h.append("set-cookie", `stable-id=${stableId.value}`);
return new Response(modifiedResponse.body, {
...modifiedResponse,
headers: h,
});
}
We'll need to set up a component in our app that will hold the embedded data
import { VercelToolbar } from "@vercel/toolbar/next";
import { Analytics } from "@vercel/analytics/next";
import type { Metadata } from "next";
import { Toaster } from "sonner";
import { DevTools } from "@/components/dev-tools";
import { Footer } from "@/components/footer";
import { Navigation } from "@/components/navigation";
import "./globals.css";
import { FlagBootstrapData } from "flags/react";
export const metadata: Metadata = {
title: "Flags SDK Example",
description: "A Flags SDK example for Ecommerce",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
<div className="bg-white">
<Navigation />
{children}
<Footer />
<DevTools />
</div>
<Toaster />
<Analytics />
<VercelToolbar />
<FlagBootstrapData />
</body>
</html>
);
}
Install Statsig Client side SDKs
pnpm i @statsig/react-bindings @statsig/web-analytics
We'll also need to update our Statsig client side provider to use the embedded data to initialize, rather than going to the network
"use client";
import type { Statsig, StatsigUser } from "@flags-sdk/statsig";
import {
LogLevel,
StatsigProvider,
useClientBootstrapInit,
} from "@statsig/react-bindings";
import { StatsigAutoCapturePlugin } from "@statsig/web-analytics";
import { useBootstrapData } from "flags/react";
import { useMemo } from "react";
function BootstrappedStatsigProvider({
user,
values,
children,
}: {
user: Parameters<typeof useClientBootstrapInit>[1];
values: Parameters<typeof useClientBootstrapInit>[2];
children: React.ReactNode;
}) {
const client = useClientBootstrapInit(
process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY as string,
user,
values,
{
logLevel: LogLevel.Debug,
plugins: [new StatsigAutoCapturePlugin()],
}
);
return <StatsigProvider client={client}>{children}</StatsigProvider>;
}
export function StaticStatsigProvider({
children,
}: {
children: React.ReactNode;
}) {
// wait for the script#embed to appear and read its contents as json
const data = useBootstrapData<{
statsigUser: StatsigUser;
clientInitializeResponse: Awaited<
ReturnType<typeof Statsig.getClientInitializeResponse>
>;
}>();
const values = useMemo(
() => (data ? JSON.stringify(data.clientInitializeResponse) : null),
[data]
);
if (!data || !values) {
return children;
}
return (
<BootstrappedStatsigProvider user={data.statsigUser} values={values}>
{children}
</BootstrappedStatsigProvider>
);
}
We'll need to set up a component in our app that will hold the embedded data
import { VercelToolbar } from "@vercel/toolbar/next";
import { Analytics } from "@vercel/analytics/next";
import type { Metadata } from "next";
import { Toaster } from "sonner";
import { DevTools } from "@/components/dev-tools";
import { Footer } from "@/components/footer";
import { Navigation } from "@/components/navigation";
import "./globals.css";
import { FlagBootstrapData } from "flags/react";
import { StaticStatsigProvider } from "@/statsig/statsig-provider";
export const metadata: Metadata = {
title: "Flags SDK Example",
description: "A Flags SDK example for Ecommerce",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
<div className="bg-white">
<Navigation />
<StaticStatsigProvider> {children} </StaticStatsigProvider>
<Footer />
<DevTools />
</div>
<Toaster />
<Analytics />
<VercelToolbar />
<FlagBootstrapData />
</body>
</html>
);
}
Now that Statsig is initialized on the client side, we can add client side exposure logging to the summer sale banner!
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags";
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";
import { generatePermutations } from "flags/next";
export async function generateStaticParams() {
const codes = await generatePermutations(productFlags);
return codes.map((code) => ({ code }));
}
export default async function Page(props: {
params: Promise<{ code: string }>;
}) {
const { code } = await props.params;
const summerBanner = await enableSummerSale(code, productFlags);
return (
<ProductDetailPageProvider>
<SummerSaleBanner show={summerBanner} />
<Main>
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
<ProductHeader />
<ImageGallery />
<div className="mt-8 lg:col-span-5">
<ColorPicker />
<SizePicker />
<AddToCart />
<ProductDetails />
</div>
</div>
</Main>
</ProductDetailPageProvider>
);
}
"use client";
import Image from "next/image";
import pool from "@/public/images/pool.jpg";
import { useStatsigClient } from "@statsig/react-bindings";
import { useEffect } from "react";
export function SummerSaleBanner({
show,
onClick,
}: {
show: boolean;
onClick?: () => void;
}) {
const statsig = useStatsigClient();
useEffect(() => {
statsig.checkGate("summer-sale");
}, []);
if (!show) return null;
return (
<div className="bg-white">
<div className="mx-auto max-w-7xl px-4 pb-8 sm:px-6 lg:px-8">
<div className="relative overflow-hidden rounded-lg">
<div className="absolute inset-0">
<Image
src={pool}
alt="Summer Sale"
className="h-full w-full object-cover object-center"
/>
</div>
<div className="relative px-6 py-32 sm:px-12 sm:py-16 lg:px-16">
<div className="relative mx-auto flex max-w-3xl flex-col items-start text-left">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
<span className="block sm:inline">Summer Sale</span>
</h2>
<p className="mt-3 text-xl text-white">
Enjoy 20% off all summer basics,
<br />
including swimwear and accessories.
</p>
<button
type="button"
className="cursor-pointer mt-8 block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto"
onClick={onClick}
>
Shop now
</button>
</div>
</div>
</div>
</div>
</div>
);
}
Exposure and event logs should now be firing, so let's head to Statsig to check the status of flag evaluations and event logs