diff --git a/flags-sdk/experimentation-statsig/app/api/bootstrap/route.ts b/flags-sdk/experimentation-statsig/app/api/bootstrap/route.ts deleted file mode 100644 index 238c3fea9..000000000 --- a/flags-sdk/experimentation-statsig/app/api/bootstrap/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getStableId } from "@/utils/get-stable-id"; -import { statsigAdapter, Statsig, type StatsigUser } from "@flags-sdk/statsig"; -import { NextResponse } from "next/server"; - -export const runtime = "edge"; - -export async function GET(request: Request): Promise { - await statsigAdapter.initialize(); - const stableId = await getStableId(); - - const user: StatsigUser = { - customIDs: { - // since we're not having authentication, we're using the stable id for targeting - // otherwise, you can define a userID at the root of the user object - stableID: stableId.value, - }, - }; - - const values = await Statsig.getClientInitializeResponse(user, { - hash: "djb2", - }); - - return NextResponse.json(values, { - headers: { - "Cache-Control": "private, max-age=60", - Vary: "Cookie", - }, - }); -} \ No newline at end of file diff --git a/flags-sdk/experimentation-statsig/app/layout.tsx b/flags-sdk/experimentation-statsig/app/layout.tsx index 0850a3820..bf69e53d6 100644 --- a/flags-sdk/experimentation-statsig/app/layout.tsx +++ b/flags-sdk/experimentation-statsig/app/layout.tsx @@ -1,6 +1,7 @@ import { VercelToolbar } from '@vercel/toolbar/next' import type { Metadata } from 'next' import { Toaster } from 'sonner' +import { FlagBootstrapData } from 'flags/react' import './globals.css' import { ExamplesBanner } from '@/components/banners/examples-banner' @@ -22,6 +23,7 @@ export default function RootLayout({ {children} + ) diff --git a/flags-sdk/experimentation-statsig/middleware.ts b/flags-sdk/experimentation-statsig/middleware.ts index 980a1aa7c..cb0b5e6dc 100644 --- a/flags-sdk/experimentation-statsig/middleware.ts +++ b/flags-sdk/experimentation-statsig/middleware.ts @@ -1,8 +1,11 @@ -import { type NextRequest, NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' import { precompute } from 'flags/next' import { productFlags } from '@/flags' import { getStableId } from './lib/get-stable-id' import { getCartId } from './lib/get-cart-id' +import { statsigAdapter } from '@flags-sdk/statsig' +import { identify } from './lib/identify' +import { embedBootstrapData } from 'flags/next' export const config = { matcher: ['/', '/cart'], @@ -19,19 +22,41 @@ export async function middleware(request: NextRequest) { request.url ) - // Add a header to the request to indicate that the stable id is generated, - // as it will not be present on the cookie request header on the first-ever request. + // Create new headers with the original request headers + const headers = new Headers(request.headers) + + // Add new headers if needed if (cartId.isFresh) { - request.headers.set('x-generated-cart-id', cartId.value) + headers.set('x-generated-cart-id', cartId.value) } if (stableId.isFresh) { - request.headers.set('x-generated-stable-id', stableId.value) + headers.set('x-generated-stable-id', stableId.value) } - // response headers - const headers = new Headers() - headers.append('set-cookie', `stable-id=${stableId.value}`) - headers.append('set-cookie', `cart-id=${cartId.value}`) - return NextResponse.rewrite(nextUrl, { request, headers }) + // Create a new request with the modified headers + const modifiedRequest = new Request(nextUrl, { ...request, headers }) + + 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}`) + h.append('set-cookie', `cart-id=${cartId.value}`) + return new Response(modifiedResponse.body, { + ...modifiedResponse, + headers: h, + }) } diff --git a/flags-sdk/experimentation-statsig/package.json b/flags-sdk/experimentation-statsig/package.json index 674e55ed7..4250552c2 100644 --- a/flags-sdk/experimentation-statsig/package.json +++ b/flags-sdk/experimentation-statsig/package.json @@ -23,7 +23,7 @@ "@vercel/toolbar": "0.1.33", "motion": "12.17.0", "clsx": "2.1.1", - "flags": "^4.0.0", + "flags": "4.1.0-c84914eb-20250613112941", "nanoid": "5.1.2", "next": "15.4.0-canary.79", "react": "^19.0.0", diff --git a/flags-sdk/experimentation-statsig/pnpm-lock.yaml b/flags-sdk/experimentation-statsig/pnpm-lock.yaml index 21ad9f6e0..e462fb423 100644 --- a/flags-sdk/experimentation-statsig/pnpm-lock.yaml +++ b/flags-sdk/experimentation-statsig/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: 2.1.1 version: 2.1.1 flags: - specifier: ^4.0.0 - version: 4.0.0(next@15.4.0-canary.79(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 4.1.0-c84914eb-20250613112941 + version: 4.1.0-c84914eb-20250613112941(next@15.4.0-canary.79(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) motion: specifier: 12.17.0 version: 12.17.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1221,8 +1221,8 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flags@4.0.0: - resolution: {integrity: sha512-CpONOK/nflKfaTT36r+Y4SP5rb9mv3AdqY/2Ws+TScrEGlT4PK+PZNiKJgIH87HooTXDblz31807q7lZ3LpuYQ==} + flags@4.1.0-c84914eb-20250613112941: + resolution: {integrity: sha512-d1NNvlJMwKPaOBOLsk9AAa2bD38A02lC1hqaLUIo6iux0LGOdL4lkRYcOI+8WjXqMjNGQQumZ8fDDPKctE56YQ==} peerDependencies: '@opentelemetry/api': ^1.7.0 '@sveltejs/kit': '*' @@ -1366,6 +1366,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + htmlrewriter@0.0.13: + resolution: {integrity: sha512-HPxT+y/i9RzNeKrJ6MBFXwB+FeTfHPfcBzoKHVnk3oiKn2s6x13tLoxjggI+0/yOSBG1n8SV//PGDBJYT/4XQw==} + http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -3463,9 +3466,10 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flags@4.0.0(next@15.4.0-canary.79(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + flags@4.1.0-c84914eb-20250613112941(next@15.4.0-canary.79(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@edge-runtime/cookies': 5.0.2 + htmlrewriter: 0.0.13 jose: 5.10.0 optionalDependencies: next: 15.4.0-canary.79(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -3585,6 +3589,8 @@ snapshots: dependencies: function-bind: 1.1.2 + htmlrewriter@0.0.13: {} + http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 diff --git a/flags-sdk/experimentation-statsig/statsig/statsig-provider.tsx b/flags-sdk/experimentation-statsig/statsig/statsig-provider.tsx index c1eeb9b72..99d22678d 100644 --- a/flags-sdk/experimentation-statsig/statsig/statsig-provider.tsx +++ b/flags-sdk/experimentation-statsig/statsig/statsig-provider.tsx @@ -1,21 +1,22 @@ 'use client' -import { Statsig } from '@flags-sdk/statsig' /** * This file exports a StatsigProvider with a client-side bootstrap. - * It requires a client-side fetch to retrieve the bootstrap payload. + * + * The bootstrap reads the data embedded by Edge Middleware. + * * Elements that determine page layout should have precomputed variants with flags-sdk. * Exposures can be logged with helpers in the `statsig-exposure` module. */ - +import type { Statsig, StatsigUser } from '@flags-sdk/statsig' import { LogLevel, StatsigProvider, useClientBootstrapInit, } from '@statsig/react-bindings' import { StatsigAutoCapturePlugin } from '@statsig/web-analytics' -import { createContext, useMemo } from 'react' -import useSWR from 'swr' +import { useBootstrapData } from 'flags/react' +import { createContext, useMemo, useState, useEffect } from 'react' export const StatsigAppBootstrapContext = createContext<{ isLoading: boolean @@ -51,35 +52,36 @@ export function StaticStatsigProvider({ }: { children: React.ReactNode }) { - const { data, error } = useBootstrap() - const values = useMemo(() => JSON.stringify(data), [data]) + // wait for the script#embed to appear and read its contents as json + const data = useBootstrapData<{ + statsigUser: StatsigUser + clientInitializeResponse: Awaited< + ReturnType + > + }>() + + const values = useMemo( + () => (data ? JSON.stringify(data.clientInitializeResponse) : null), + [data] + ) - if (!data) { + if (!data || !values) { return ( - + {children} ) } return ( - - + + {children} ) } - -const fetcher = (url: string) => - fetch(url, { - headers: { - Vary: 'Cookie', - }, - }).then((res) => res.json()) - -export function useBootstrap() { - return useSWR< - Awaited> - >('/api/bootstrap', fetcher) -}