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>
Basic Usage 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,
} from "@statsig/react-bindings";
import { StatsigAutoCapturePlugin } from '@statsig/web-analytics';
export default function MyStatsig({ children }: { children: React.ReactNode }) {
return (
<StatsigProvider
sdkKey={process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY!}
user={{ userID: "a-user" }}
options={{
logLevel: LogLevel.Debug,
plugins: [ new StatsigAutoCapturePlugin() ],
}}>
{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">
<body>
<MyStatsig>
{children}
</MyStatsig>
</body>
</html>
);
}
To integrate Statsig into your Page Router app you can add the StatsigProvider
to your _app.tsx
file.
There is a full example in the samples directory of the javascript sdk.
// pages/_app.tsx
import { useEffect } from "react";
import type { AppProps } from "next/app";
import {
LogLevel,
StatsigProvider,
} from "@statsig/react-bindings";
import { StatsigAutoCapturePlugin } from '@statsig/web-analytics';
export default function App({ Component, pageProps }: AppProps) {
return (
<StatsigProvider
sdkKey={process.env.NEXT_PUBLIC_STATSIG_CLIENT_KEY!}
user={{ userID: "a-user" }}
options={{
logLevel: LogLevel.Debug,
plugins: [ new StatsigAutoCapturePlugin() ],
}}>
{children}
</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>
);
}
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.
Advanced Setup
We offer deeper integrations with Next.js that improve the performance and stability of your Statsig integration. If you were just trying to log an event or check your first gate, your project is configured and ready to go with the above basic setup instructions.
Client Bootstrapping (Recommended)
Bootstrapping is a method of keeping updated values on your server (in the case of Next, your node webserver), and sending them down with the frontend code when a request is made. This has the advantage of preventing an additional network request before your content is displayed, improving your site's responsiveness. This also enables Statsig usage on server-rendered components. While the performance gains are appealing, bootstrapping requires some additional setup effort, and you must be mindful of which code you are running server side and client side.
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,
useClientBootstrapInit, // <- Add this
} from "@statsig/react-bindings";
import { StatsigAutoCapturePlugin } 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 auto initializing in the provider
const client = useClientBootstrapInit(
bootstrapValues.key,
bootstrapValues.user,
bootstrapValues.data,
{
logLevel: LogLevel.debug,
plugins: [ new StatsigAutoCapturePlugin() ]
}
);
return <StatsigProvider client={client}>{children}</StatsigProvider>;
}
If you load the app now, you should see the same values as your previous implementation, this time without any additional network requests.
Managing StableIDs
Statsig generates StableIDs as a pseudo-ID for logged-out experiments and user management. StableIDs are generated client-side, but when boostrapping, values are generated on the server, creating undesirable side-effects like stableIDs regenerating more than logical for any one device/user. A simple cookie can solve this problem, with an implementation pattern suggested here.
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
import { getStatsigServerProps } from "./statsig-backend";
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
import {
LogLevel,
StatsigProvider,
StatsigUser,
} from "@statsig/react-bindings";
import { runStatsigAutoCapture } from '@statsig/web-analytics';
import React, { useEffect } from "react";
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 (Optional)
If you want to harden your integration and protect your feature gating/experimentation values/event logging from being blocked by ad blockers, you can set up a network proxy.
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/log_event/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/log_event.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { LogEventObject } from 'statsig-node';
import { logEvents } from '../../../lib/statsig-backend';
type LogEventBody = {
events: LogEventObject[];
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<string>
) {
if (req.method !== 'POST') {
res.status(400).send('/log_event only supports POST');
return;
}
const body: unknown = req.body;
if (!body || typeof body !== 'string') {
res.status(400).send('Request body is required');
return;
}
const { events } = JSON.parse(body) as LogEventBody;
await logEvents(events);
res.status(202).send('{"success": true}');
}
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: {
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: {
logEventUrl: "/statsig-proxy/log_event",
initializeUrl: "/statsig-proxy/initialize",
},
disableCompression: true,
disableStatsigEncoding: true,
// Optional - Prints debug logs to the console
logLevel: LogLevel.Debug
}
);