Statsig in Next.js
Supported Features
Installation
To use Statsig with Next.js, install the @statsig/react-bindings
and @statsig/web-analytics
packages:
- NPM
- Yarn
- PNPM
npm install @statsig/react-bindings @statsig/web-analytics
yarn add @statsig/react-bindings @statsig/web-analytics
pnpm add @statsig/react-bindings @statsig/web-analytics
Add your Statsig client key to your .env.local
file. You can find your key at which you can find at console.statsig.com/api_keys.
Note: The NEXT_PUBLIC_
prefix is required for this to be available on the client side.
# .env.local
NEXT_PUBLIC_STATSIG_CLIENT_KEY=client-<REPLACE_WITH_YOUR_CLIENT_KEY>
Integrating with Next.js
Statsig supports both the Page Router and App Router in Next.js. There are some differences in how you integrate Statsig into each.
- App Router
- Page Router
To integrate Statsig into your App Router app you need to use the
use client
directive
to render the StatsigProvider
on the client side.
// app/my-statsig.tsx
"use client";
import React, { useEffect } from "react";
import {
LogLevel,
StatsigProvider,
useClientAsyncInit,
} from "@statsig/react-bindings";
import { runStatsigAutoCapture } from "@statsig/web-analytics";
export default function MyStatsig({ children }: { children: React.ReactNode }) {
const { client } = useClientAsyncInit(
process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY!,
{ userID: "a-user" },
{ logLevel: LogLevel.Debug } // Optional - Prints debug logs to the console
);
useEffect(() => {
runStatsigAutoCapture(client);
}, [client]);
return <StatsigProvider client={client}>{children}</StatsigProvider>;
}
Then, use this component in your Root layout.tsx
file.
// app/layout.tsx
import MyStatsig from "./my-statsig";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<MyStatsig>
<body>
{children}
</body>
</MyStatsig>
</html>
);
}
To integrate Statsig into your Page Router app you can add the StatsigProvider
to your _app.tsx
file.
// pages/_app.tsx
import { useEffect } from "react";
import type { AppProps } from "next/app";
import {
LogLevel,
StatsigProvider,
useClientAsyncInit,
} from "@statsig/react-bindings";
import { runStatsigAutoCapture } from "@statsig/web-analytics";
export default function App({ Component, pageProps }: AppProps) {
const { client } = useClientAsyncInit(
process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY!,
{ userID: "a-user" },
{ logLevel: LogLevel.Debug } // Optional - Prints debug logs to the console
);
useEffect(() => {
runStatsigAutoCapture(client);
}, [client]);
return (
<StatsigProvider client={client}>
<Component {...pageProps} />
</StatsigProvider>
);
}
Now if you load your app, you should see events being sent to Statsig and if you have LogLevel set to Debug, you should see debug logs in the browsers console.
Note: You should remove the logLevel
option before deploying your app to production.
Checking a Gate
- App Router
- Page Router
'use client';
import { useGateValue } from "@statsig/react-bindings";
export default function Home() {
const gate = useGateValue("my_gate");
return (
<div>
Gate Value: {gate ? 'PASSED' : 'FAILED'}
</div>
);
}
Note: In an App Router app, you need to use the
use client
directive to ensure your logic runs on the frontend.
import { useGateValue } from "@statsig/react-bindings";
export default function Home() {
const gate = useGateValue("my_gate");
return (
<div>
Gate Value: {gate ? 'PASSED' : 'FAILED'}
</div>
);
}
Getting a Dynamic Config
- App Router
- Page Router
'use client';
import { useDynamicConfig } from "@statsig/react-bindings";
export default function Home() {
const config = useDynamicConfig("my_dynamic_config");
return (
<div>
Title: {config.get('title', 'Fallback Title')}
</div>
);
}
Note: In an App Router app, you need to use the
use client
directive to ensure your logic runs on the frontend.
import { useDynamicConfig } from "@statsig/react-bindings";
export default function Home() {
const config = useDynamicConfig("my_dynamic_config");
return (
<div>
Title: {config.get('title', 'Fallback Title')}
</div>
);
}
Getting an Experiment
- App Router
- Page Router
'use client';
import { useExperiment, useLayer} from "@statsig/react-bindings";
export default function Home() {
const layer = useLayer("my_experiment_layer");
// or
const experiment = useExperiment("my_experiment");
return (
<div>
Title: {layer.get('title', 'Fallback Title')}
{/* or */}
Title: {experiment.get('title', 'Fallback Title')}
</div>
);
}
Note: In an App Router app, you need to use the
use client
directive to ensure your logic runs on the frontend.
import { useExperiment, useLayer} from "@statsig/react-bindings";
export default function Home() {
const layer = useLayer("my_experiment_layer");
// or
const experiment = useExperiment("my_experiment");
return (
<div>
Title: {layer.get('title', 'Fallback Title')}
{/* or */}
Title: {experiment.get('title', 'Fallback Title')}
</div>
);
}
Getting a Parameter Store
- App Router
- Page Router
'use client';
import { useParameterStore} from "@statsig/react-bindings";
export default function Home() {
const store = useParameterStore("my_param_store");
return (
<div>
Title: {store.get('title', 'Fallback Title')}
</div>
);
}
Note: In an App Router app, you need to use the
use client
directive to ensure your logic runs on the frontend.
import { useParameterStore} from "@statsig/react-bindings";
export default function Home() {
const store = useParameterStore("my_param_store");
return (
<div>
Title: {store.get('title', 'Fallback Title')}
</div>
);
}
Logging an Event
- App Router
- Page Router
'use client';
import { useStatsigClient } from "@statsig/react-bindings";
export default function Home() {
const { client } = useStatsigClient();
return (
<div>
<button onClick={() => client.logEvent("my_custom_event")}>
Click Me
</button>
</div>
);
}
Note: In an App Router app, you need to use the
use client
directive to ensure your logic runs on the frontend.
import { useStatsigClient } from "@statsig/react-bindings";
export default function Home() {
const { client } = useStatsigClient();
return (
<div>
<button onClick={() => client.logEvent("my_custom_event")}>
Click Me
</button>
</div>
);
}
Client Bootstrapping
To fully take advantage of Next.js with Statsig, we should bootstrap the Statsig client with the values it needs. This way, the client does not need to make a separate network request to fetch these values.
Install statsig-node
To generate the required values, we can use the Statsig server SDK (statsig-node
) on our backend.
- NPM
- Yarn
- PNPM
npm install statsig-node
yarn add statsig-node
pnpm add statsig-node
next, add a Server key to your .env
file. Note that this one is private and does not start with NEXT_PUBLIC_
.
You can find your key at which you can find at console.statsig.com/api_keys.
# .env.local
NEXT_PUBLIC_STATSIG_CLIENT_KEY= # No Change
STATSIG_SERVER_KEY=secret-<REPLACE_WITH_YOUR_SERVER_KEY> # <- Added this line
Integrate the Backend Logic
- App Router
- Page Router
In our App Router example, we should add a new file statsig-backend.ts
to our app
folder.
This file will contain the logic to initialize our Statsig server SDK and generate the bootstrap values.
// app/statsig-backend.ts
import Statsig, { StatsigUser } from "statsig-node";
const isStatsigReady = Statsig.initialize(process.env.STATSIG_SERVER_KEY!, {
environment: { tier: "development" },
});
export async function generateBootstrapValues(): Promise<{
data: string;
user: StatsigUser;
key: string;
}> {
await isStatsigReady;
const user = { userID: "a-user" };
const key = process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY!;
const values = Statsig.getClientInitializeResponse(user, key, {
hash: "djb2", //🔥 'djb2' is required. By default this would be 'sha256'.
});
return {
data: JSON.stringify(values),
user,
key,
};
}
In our Page Router example, we should add a new file statsig-backend.ts
to our pages
folder
This file will contain the logic to initialize our Statsig server SDK and generate the bootstrap values.
// pages/statsig-backend.ts
import Statsig, { StatsigUser } from "statsig-node";
export const isStatsigReady = Statsig.initialize(
process.env.STATSIG_SERVER_KEY!,
{
environment: { tier: "development" },
}
);
export type StatsigServerProps = {
user: StatsigUser;
data: string;
key: string;
};
export async function getStatsigServerProps(): Promise<StatsigServerProps> {
await isStatsigReady;
const user = { userID: "a-user" };
const key = process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY!;
const values = Statsig.getClientInitializeResponse(user, key, {
hash: "djb2", //🔥 'djb2' is required. By default this would be 'sha256'.
});
return {
data: JSON.stringify(values),
user,
key,
};
}
Apply the Bootstrap Values
- App Router
- Page Router
We now need to refactor our Root layout.tsx
to call the generateBootstrapValues
function and pass it to the client side.
// app/layout.tsx
// ...
import { generateBootstrapValues } from "./statsig-backend";
// ...
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const bootstrapValues = await generateBootstrapValues();
return (
<html lang="en">
<MyStatsig bootstrapValues={bootstrapValues}>
<body>
{children}
</body>
</MyStatsig>
</html>
);
}
Finally, update the MyStatsig
component to accept the bootstrapValues
prop,
swapping out useClientAsyncInit for useClientBootstrapInit.
// app/my-statsig.tsx
"use client";
import {
LogLevel,
StatsigProvider,
StatsigUser,
// useClientAsyncInit, // <- Remove this
useClientBootstrapInit, // <- Add this
} from "@statsig/react-bindings";
import { runStatsigAutoCapture } from "@statsig/web-analytics";
import React, { useEffect } from "react";
export default function MyStatsig({
children,
bootstrapValues,
}: {
bootstrapValues: { data: string; user: StatsigUser; key: string };
children: React.ReactNode;
}) {
// Update to using useClientBootstrapInit instead of useClientAsyncInit
const client = useClientBootstrapInit(
bootstrapValues.key,
bootstrapValues.user,
bootstrapValues.data,
{ logLevel: LogLevel.Debug } // Optional - Prints debug logs to the console
);
useEffect(() => {
runStatsigAutoCapture(client);
}, [client]);
return <StatsigProvider client={client}>{children}</StatsigProvider>;
}
If you load the app now, you should see that same as before, but this time we aren't issuing any unnecessary requests.
Since we want to interact with our backend script to generate values before they reach the client side,
we need to utilize Page Router's getServerSideProps
. This will ensure that our logic
only runs on the backend.
On any page you wish to use Statsig, you can add a call to our getStatsigServerProps
function.
Here is an example of using it in our Home page:
// pages/index.tsx
export const getServerSideProps = async () => {
const statsigProps = await getStatsigServerProps();
return { props: { statsigProps } };
};
export default function Home() {
// No Change
}
With these props generated, we then need to pass them to the StatsigClient in our _app.tsx
file.
We do this conditionally, so that Statsig only runs on pages that call getStatsigServerProps
.
// pages/_app.tsx
export default function App({ Component, pageProps }: AppProps) {
const clientRef = useRef<StatsigClient | null>();
const client = useMemo(() => {
if (!pageProps.statsigProps) {
return null;
}
if (clientRef.current) {
return clientRef.current;
}
const { key, user, data } = pageProps.statsigProps;
const inst = new StatsigClient(
key,
user,
{ logLevel: LogLevel.Debug } // Optional - Prints debug logs to the console
);
clientRef.current = inst;
inst.dataAdapter.setData(data);
inst.initializeSync();
runStatsigAutoCapture(inst);
return inst;
}, [pageProps.statsigProps, clientRef.current]);
if (client) {
return (
<StatsigProvider client={client}>
<Component {...pageProps} />
</StatsigProvider>
);
}
return <Component {...pageProps} />;
}
Proxying Network Traffic
It is possible to route all Statsig network traffic through your Next.js server. There are a few reasons why you might want to set this up.
- Avoid ad blockers
- Keep network traffic within your own cluster
- Maintain your own event filtering/de-duplication logic
- App Router
- Page Router
The Statsig client uses two main endpoints. /initialize
and /log_event
.
We will need to setup Next.js Route Handlers
for these.
For the sake of this demo, we will house them under app/statsig-proxy
.
The Statsig client uses two main endpoints. /initialize
and /log_event
.
We will need to setup Next.js API Routes
for these. For the sake of this demo, we will house them under api/statsig-proxy
.
Add Route /initialize
- App Router
- Page Router
// app/statsig-proxy/initialize/route.ts
import { StatsigUser } from "statsig-node";
import { generateBootstrapValues } from "@/app/statsig-backend";
export async function POST(request: Request): Promise<Response> {
const json = await request.json();
if (!json || typeof json !== "object") {
return new Response(null, { status: 400 });
}
const body = json as { user: StatsigUser };
const { data } = await generateBootstrapValues(body.user);
return new Response(data);
}
// pages/api/statsig-proxy/initialize.ts
import { getStatsigValues } from "@/pages/statsig-backend";
import type { NextApiRequest, NextApiResponse } from "next";
import { StatsigUser } from "statsig-node";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<string>
) {
if (req.method !== "POST") {
res.status(400).send("/initialize only supports POST");
return;
}
const { user } = JSON.parse(req.body) as { user: StatsigUser };
const values = await getStatsigValues(user);
res.status(200).send(values);
}
Add Route /log_event
- App Router
- Page Router
// app/statsig-proxy/initialize/route.ts
import { LogEventObject } from "statsig-node";
import { logEvents } from "@/app/statsig-backend";
type LogEventBody = {
events: LogEventObject[];
};
export async function POST(request: Request): Promise<Response> {
const json = await request.json();
if (!json || typeof json !== "object" || !Array.isArray(json.events)) {
return new Response(null, { status: 400 });
}
const body = json as LogEventBody;
await logEvents(body.events);
return new Response(JSON.stringify({ success: true }));
}
// pages/api/statsig-proxy/initialize.ts
import { getStatsigValues } from "@/pages/statsig-backend";
import type { NextApiRequest, NextApiResponse } from "next";
import { StatsigUser } from "statsig-node";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<string>
) {
if (req.method !== "POST") {
res.status(400).send("/initialize only supports POST");
return;
}
const { user } = JSON.parse(req.body) as { user: StatsigUser };
const values = await getStatsigValues(user);
res.status(200).send(values);
}
Assign Urls
Now that we have endpoints for handling /initialize
and /log_event
,
we need to configure that Statsig client to use them.
- App Router
- Page Router
In our app/my-statsig.tsx
file, where we created our StatsigClient, we can pass in some extra StatsigOptions.
const inst = new StatsigClient(
clientSdkKey,
user,
{
networkConfig: {
networkConfig: {
logEventUrl: "/statsig-proxy/log_event",
initializeUrl: "/statsig-proxy/initialize",
},
disableCompression: true,
disableStatsigEncoding: true,
},
// Optional - Prints debug logs to the console
logLevel: LogLevel.Debug
}
);
In our pages/_app.tsx
file, where we created our StatsigClient, we can pass in some extra StatsigOptions.
const inst = new StatsigClient(
clientSdkKey,
user,
{
networkConfig: {
networkConfig: {
logEventUrl: "/statsig-proxy/log_event",
initializeUrl: "/statsig-proxy/initialize",
},
disableCompression: true,
disableStatsigEncoding: true,
},
// Optional - Prints debug logs to the console
logLevel: LogLevel.Debug
}
);
Session Replay
By including the @statsig/session-replay
package in your project, you can automatically capture and log user sessions as videos.
This feature is useful for debugging and understanding user behavior. Read more about Session Replay.
Web Analytics / Auto Capture
By including the @statsig/web-analytics
package in your project, you can automatically capture common web events like clicks and page views.
Read more about Web Analytics.